项目模块的命名以及目录结构

一、项目的命名规则

PS : 项目的基础包路径和项目的命名一致

1 . 服务接口 : 项目名称 + 逻辑层名称 + 模块名称 + interfaces

服务接口例子 : banniu.data.salecrm.interfaces
基础包路径就是 : banniu.data.salecrm.interfaces

服务接口后面的interfaces是固定的

2 . 提供者 : 项目名称 + 逻辑层名称 + 模块名称

提供者例子 : banniu.data.salecrm

1) 项目名称 : banniu
2) 逻辑层名称 : data (数据层)
3) 模块名称 : salecrm

基础包路径就是 : banniu.data.salecrm

PS : 逻辑层名称暂时是固定的 , 包含 data(数据层)、app(应用层)、web(控制层)

3 . 消费者 : banniu.web

1) 项目名称 : banniu
2) 逻辑层名称 : web (控制层)

基础包路径就是 : banniu.web

4 . 实体类包名命名规则 : 项目名称 + model + 模块名称

例子 : banniu.model.salecrm

PS : model是固定的 .

二、项目各逻辑层接口命名方式

1 . 数据层命名

interface命名 : 功能点名称 + DataService
实现类命名    :  功能点名称 +  DataServiceImpl

2 . 应用层命名

interface命名 : 功能点名称 + AppService
实现类命名    :  功能点名称 +  AppServiceImpl

3 . 控制层命名

controller命名 : 功能点名称 + Controller

4 . 举例说明

数据层例子 : TopicDataService --> Topic 是分享功能名称

应用层例子 : TopicAppService --> Topic 是分享功能名称

控制层例子 : TopicController --> Topic是分享功能名称

三、项目的目录结构

1. 服务接口

PS : 服务接口是一个jar包 , 被提供者和消费者共同依赖

2. 提供者

PS : 发布脚本放在服务器上面 .不在本文章里面写了

3. web层

单元测试

单元测试分为数据层 , 应用层 , 控制层 ; 每一层的测试都是独立的单元测试 , 不依赖其他项目 , 尽管逻辑上面是依赖的 . 对于数据层的测试 , 我们采用事务回滚的方式进行测试 , 这样就不会对数据库造成脏数据 ; 对于应用层和控制层 , 我们采用Mock的打桩方式进行单元测试 .

预准备

1 . 如何快速创建Test类

在IDEA里有一种快速创建的方式 , 只需要你找到你需要测试的类 , 然后把鼠标移到类名上 , 使用快捷键 Alt + Enter 快捷键 , 就可以看到 Create Test 的选项 . 点进去就可以创建你的测试类了 , 进入之后 Testing library 选择Junit 4 .

数据层测试

1 . 生成测试类之后 , 首先在你的测试类上面添加注解 :

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration("classpath:spring/spring-dao.xml") 注意 : @ContextConfiguration 括号里面的内容写的是你的本地dao层的配置文件

@Transactional() // 前提配置文件中已经配置了事务

@Rollback // 这个配置针对于所有回滚的 , 放在类上面表示类里面的方法都会回滚 . 也可以单独放在某一个方法上面

下面是一个真实的测试的代码 :

package banniu.data.salecrm.impl;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import banniu.data.salecrm.interfaces.CustomerLogsDataService;
import banniu.model.salecrm.CustomerLogsDo;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import static org.junit.Assert.*;

/**
 * Created by Xstarfct on 2017/3/15.
 */

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-dao.xml")
//支持回滚 , 数据库表的存储引擎必须是支持事务的
@Transactional(transactionManager = "transactionManager")
@Rollback
public class CustomerLogsDataServiceImplTest {

    @Autowired
    private CustomerLogsDataService customerLogsDataService;

    @Test
    public void createCustomerLogs() throws Exception {
        CustomerLogsDo logsDo = new CustomerLogsDo();
        logsDo.setId(888L);
        logsDo.setCompanyId(36L);
        logsDo.setAfterUserId(111L);
        logsDo.setContent("这是一条测试的日志");
        logsDo.setLogType(0);
        logsDo.setCustomerId(1111L);
        logsDo.setUserId(111L);
        logsDo.setReasonTagId(111L);
        boolean createResult = customerLogsDataService.createCustomerLogs(logsDo);
        System.out.println("创建结果:" + createResult);
        logsDo = customerLogsDataService.loadCustomerLogsById(888L);
        System.out.println(logsDo);
    }

    @Test
    public void getPageListByCustomerLogsDo() throws Exception {

    }

    @Test
    public void updateCustomerLogsById() throws Exception {

    }

    @Test
    public void getLogsByFields() throws Exception {

        Map<String,Object> map =new HashMap();
        Date  bD = null;
        Date  eD = null;
        try {
            bD =  new SimpleDateFormat("yyyy-MM-dd").parse("2017-03-20");
            map.put("beginDate", bD);
        }catch (ParseException e)
        {
            System.out.println(e.getMessage());
        }

        try {
            eD =  new SimpleDateFormat("yyyy-MM-dd").parse("2017-03-22");
            map.put("endDate", eD);
        }catch (ParseException e)
        {
            System.out.println(e.getMessage());
        }

        map.put("beginDate", bD);
        map.put("endDate", eD);
        customerLogsDataService.getLogsByFields(map, 1, 10);
    }

}

应用层测试

2 . 应用层的测试类是需要Mock进行打桩的 .

所谓的打桩就是模仿一个类 , 被模仿的类使用@Mock进行注解 ;

使用方法 : 比如现在有一个 CustomerAppServiceImpl 需要测试

 1. 测试 getPageListByCustomer 方法 . 这个方法里面使用 CustomerDataService, 由于这个类是数据层的方法 , 所以我们要对它进行模仿 .
 这个地方就要使用@Mock对它进行注解 , 然后在setUp方法里面使用构造方法将我们Mock好的对象传参到被测试的类中

 2. 模仿了之后 ,我们需要对调用CustomerDataService的方法进行值的返回 , 保证 getPageListByCustomer 方法可以正常执行 . 我们使用when().thenReturn()方法进行模拟 . when里面是CustomerDataService调用方法的方式 , thenReturn里面返回一个符合的数据类型 .

 3. 我们可以对返回的值进行assert ,这个方法是Junit提供的 .

代码展现 :

package banniu.app.salecrm.impl;

import banniu.data.common.interfaces.BanNiuGlobalIdService;
import banniu.data.salecrm.interfaces.CustomerDataService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import banniu.app.salecrm.interfaces.CustomerAppService;
import banniu.app.salecrm.interfaces.TagsAppService;
import banniu.common.util.Pagination;
import banniu.model.salecrm.CustomerDo;

import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;

/**
 * Created by Xstarfct on 2017/3/6.
 * @author chuanshanjia
 * @version 1.0
 * @since 2017-03-06 14:41:34
 */
public class CustomerAppServiceImplTest {

    private CustomerAppService customerAppService;

    @Mock    //对这个类进行模仿
    private CustomerDataService customerDataService;

    @Mock
    private BanNiuGlobalIdService banNiuGlobalIdService;

    /**
     *  在所有test之前执行
     */
    @Before
    public void setUp() {
        //初始化Mock
        MockitoAnnotations.initMocks(this);
        //对测试的service进行初始化 , 需要把Mock的对象传入到被测试的类中
        customerAppService = new CustomerAppServiceImpl(customerDataService, banNiuGlobalIdService);
    }

    @Test
    public void testPageList() {
        //模仿方法的返回值
        when(customerDataService.getPageListByCustomerDo(any(CustomerDo.class),anyInt(),anyInt())).thenReturn(null);

        CustomerDo customerDo = new CustomerDo();
        customerDo.setCompanyId(36L);
        Pagination<CustomerDo> customerDos = customerAppService.getPageListByCustomer(customerDo, 1, 10);

        //进行assert
        assertNull(customerDos);
    }

}

控制层测试

3 . 控制层的单元测试需要不仅仅需要Mock对象还要模拟http请求 .

1. 对被测试的类进行注解
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration    //一定要加上 , 是为了模拟web服务器请求的
@ContextConfiguration(locations = {"classpath:spring/applicationContext.xml","classpath:dubbo/dubbo-consumer.xml"}) //加载spring的配置 , 和web.xml加载配置的原理一直

2. 在测试类里面添加MockMvc的类 . 这个类是用于模拟http请求 .

3. 在测试的方法中使用mockMvc.perform()方法 , 这个方法里面的参数包含设置accept , Content-Type , 以及使用GET , POST, PUT等方法 . 返回值是ResultActions
然后通过返回值是ResultActions 可以获取到请求的结果 , 如果出现错误还可以把错误打印出来 .

示例代码如下 :

package banniu.web.controller.salecrm;

import banniu.app.salecrm.interfaces.TagsAppService;
import banniu.common.util.Pagination;
import banniu.model.salecrm.TagsDo;
import banniu.web.utils.SalecrmConstant;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import wjb.model.user.UserDo;

import java.util.Date;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * @author leiniao
 */

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {"classpath:spring/applicationContext.xml","classpath:dubbo/dubbo-consumer.xml"})
public class TagControllerTest {

    private MockMvc mockMvc;

    private Gson gson = new Gson();

    @Mock // 将需要调用的service Mock一下
    private TagsAppService tagsAppService;

    @InjectMocks //使mock对象的使用类可以注入mock对象,在上面这个例子中,mock对象是TagsAppService,使用了TagsAppService的是TagController,所以在TagController加上该Annotate;
    private TagController tagController;

    @Before
    public void setUp() {
        //初始化
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(tagController).build();
    }

    private TagsDo getMockTag() {
        TagsDo tagsDo = new TagsDo();
        tagsDo.setId(12121L);
        tagsDo.setCompanyId(36L);
        tagsDo.setGmtCreate(new Date(1489982400000L));
        tagsDo.setStatus(1);
        tagsDo.setTagKey("key");
        tagsDo.setTagName("标签");
        tagsDo.setTagPosition(3);
        tagsDo.setTagType(4);
        tagsDo.setUserId(729L);
        return tagsDo;
    }

    private Pagination<TagsDo> getMockPagination() {
        String json = "{\"total\":\"14\",\"pageSize\":10,\"pageNum\":1,\"results\":[{\"id\":\"149113492545002186\",\"tagId\":\"149093978058808048\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"有意向\",\"tagType\":1,\"tagKey\":\"youyixiang\",\"status\":0,\"tagPosition\":1,\"gmtCreate\":\"2017-03-16 17:22:53\",\"gmtModified\":\"2017-03-16 17:22:53\"},{\"id\":\"149104071532407641\",\"tagId\":\"149093888355806135\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"高端客户\",\"tagType\":1,\"tagKey\":\"gaoduan\",\"status\":0,\"tagPosition\":3,\"gmtCreate\":\"2017-03-15 15:12:43\",\"gmtModified\":\"2017-03-15 15:13:44\"},{\"id\":\"149104071532403726\",\"tagId\":\"149093888355806135\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"中端客户\",\"tagType\":1,\"tagKey\":\"zhongduan\",\"status\":0,\"tagPosition\":2,\"gmtCreate\":\"2017-03-15 15:12:43\",\"gmtModified\":\"2017-03-15 15:13:26\"},{\"id\":\"149104071532400364\",\"tagId\":\"149093888355806135\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"低端客户\",\"tagType\":1,\"tagKey\":\"diduan\",\"status\":0,\"tagPosition\":1,\"gmtCreate\":\"2017-03-15 15:12:43\",\"gmtModified\":\"2017-03-15 15:12:43\"},{\"id\":\"149104067228201735\",\"tagId\":\"149093588738404980\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"电话\",\"tagType\":1,\"tagKey\":\"source_dianhua\",\"status\":0,\"tagPosition\":2,\"gmtCreate\":\"2017-03-15 15:12:00\",\"gmtModified\":\"2017-03-15 15:12:00\"},{\"id\":\"149093883885005724\",\"tagId\":\"149093588738404980\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"官网\",\"tagType\":1,\"tagKey\":\"guanwang\",\"status\":0,\"tagPosition\":1,\"gmtCreate\":\"2017-03-14 10:56:02\",\"gmtModified\":\"2017-03-15 15:11:39\"},{\"id\":\"149093604927608240\",\"tagId\":\"149093600039207588\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"有意向\",\"tagType\":1,\"tagKey\":\"youyixiang\",\"status\":0,\"tagPosition\":2,\"gmtCreate\":\"2017-03-14 10:09:33\",\"gmtModified\":\"2017-03-15 15:10:52\"},{\"id\":\"149104055406708678\",\"tagId\":\"149093978058808048\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"上门\",\"tagType\":1,\"tagKey\":\"shangmen\",\"status\":0,\"tagPosition\":2,\"gmtCreate\":\"2017-03-15 15:10:02\",\"gmtModified\":\"2017-03-15 15:10:02\"},{\"id\":\"149094024196002754\",\"tagId\":\"149093600039207588\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"已成交\",\"tagType\":1,\"tagKey\":\"yichengjiao\",\"status\":0,\"tagPosition\":1,\"gmtCreate\":\"2017-03-14 11:19:25\",\"gmtModified\":\"2017-03-14 11:19:25\"},{\"id\":\"149094019745705822\",\"tagId\":\"149093978058808048\",\"companyId\":\"36\",\"userId\":\"110119744\",\"tagName\":\"电话\",\"tagType\":1,\"tagKey\":\"mobile\",\"status\":0,\"tagPosition\":1,\"gmtCreate\":\"2017-03-14 11:18:41\",\"gmtModified\":\"2017-03-14 11:18:41\"}]}";

        return gson.fromJson(json, new TypeToken<Pagination<TagsDo>>(){}.getType());
    }

    private UserDo getMockUser() {
        UserDo userDo = new UserDo();
        userDo.setCompanyId(36L);
        userDo.setId(729);

        return userDo;
    }

    @Test
    public void get() throws Exception {

        when(tagsAppService.getTagsById(anyLong())).thenReturn(getMockTag());

        ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/tag/{id}", 121212).accept(MediaType.APPLICATION_JSON_UTF8));

        MvcResult mvcResult = resultActions.andReturn();

        Exception exception = mvcResult.getResolvedException();
        if(exception != null) {
            exception.printStackTrace();
        }

        String responseBody = mvcResult.getResponse().getContentAsString();

        System.out.println(responseBody);

    }

    @Test
    public void list() throws Exception {

        //当调用这个方法的时候 , 返回一个Mock的对象
        when(tagsAppService.getPageListByTags(any(TagsDo.class), anyInt(), anyInt())).thenReturn(getMockPagination());

        // 给@RequestBody 注解传参数 , Json格式
        String requestBody = "{\"pageNum\":\"1\",\"pageSize\":\"20\"}";

        //模拟http请求发送一个请求
        ResultActions resultActions = mockMvc.perform(patch("/tag")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .content(requestBody)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .sessionAttr(SalecrmConstant.SESSION_USER, getMockUser()));

        //获取返回结果
        MvcResult mvcResult = resultActions.andReturn();

        //如果有错误 , 把错误信息的堆栈信息打出来
        Exception exception = mvcResult.getResolvedException();
        if(exception != null) {
            exception.printStackTrace();
        }

        //获取结果的Body
        String responseBody = mvcResult.getResponse().getContentAsString();

        System.out.println(responseBody);

    }

    @Test
    public void add() throws Exception {

        when(tagsAppService.createTags(any())).thenReturn(getMockTag());

        String requestBody = "{\"tagId\":\"1\",\"companyId\":\"36\",\"userId\":\"729\",\"tagName\":\"测试\",\"tagType\":\"1\",\"tagKey\":\"1\",\"status\":\"1\",\"tagPosition\":\"1\"}";
        RequestBuilder requestBuilder = post("/tag")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(requestBody)
                .sessionAttr(SalecrmConstant.SESSION_USER, getMockUser());

        ResultActions resultActions = mockMvc.perform(requestBuilder);
        resultActions.andExpect(status().isOk());
        MvcResult mvcResult = resultActions.andReturn();

        //如果有错误 , 把错误信息的堆栈信息打出来
        Exception exception = mvcResult.getResolvedException();
        if(exception != null) {
            exception.printStackTrace();
        }
        String responseBody = mvcResult.getResponse().getContentAsString();
        System.out.println(responseBody);
    }

    @Test
    public void update() throws Exception {
        when(tagsAppService.updateTags(any(TagsDo.class))).thenReturn(true);

        String requestBody = "{\"tagId\":\"1\",\"companyId\":\"36\",\"userId\":\"729\",\"tagName\":\"测试\",\"tagType\":\"1\",\"tagKey\":\"1\",\"status\":\"1\",\"tagPosition\":\"1\"}";
        RequestBuilder requestBuilder = put("/tag/{id}",2323)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(requestBody)
                .sessionAttr(SalecrmConstant.SESSION_USER, getMockUser());

        ResultActions resultActions = mockMvc.perform(requestBuilder);
        resultActions.andExpect(status().isOk());
        MvcResult mvcResult = resultActions.andReturn();

        //如果有错误 , 把错误信息的堆栈信息打出来
        Exception exception = mvcResult.getResolvedException();
        if(exception != null) {
            exception.printStackTrace();
        }
        String responseBody = mvcResult.getResponse().getContentAsString();
        System.out.println(responseBody);

    }

    @Test
    public void delete() throws Exception {

        RequestBuilder requestBuilder = MockMvcRequestBuilders.delete("/tag/{id}",2323)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8);

        ResultActions resultActions = mockMvc.perform(requestBuilder);
        resultActions.andExpect(status().isOk());
        MvcResult mvcResult = resultActions.andReturn();

        //如果有错误 , 把错误信息的堆栈信息打出来
        Exception exception = mvcResult.getResolvedException();
        if(exception != null) {
            exception.printStackTrace();
        }
        String responseBody = mvcResult.getResponse().getContentAsString();
        System.out.println(responseBody);
    }

}

上面的代码对我们现在所用到的请求类型都做了demo .

结束语

对于mapper的具体的配置 , 请参照 Spring + Mybatis 整合