关于 junit
和 mockito
的相关知识, 如果说junit提供了测试方法, 那么mockito则是提供了bean的管理以及bean的方法调用 , 如果这两块某些注解和方法的用法不熟悉的话 , 介意看看这篇文章 : https://www.jianshu.com/p/ecbd7b5a2021
内容很全, 必须先铺垫一下.
下面就开始本文内容了 . spring-test的话主要依赖于 mockito
和 junit
这两个库.
Controller测试一般是web接口测试 , 但是往往依赖于service层,所以不适合做测试. 但是提供了mock 很好的帮助了我们测试.
我们这里就拿登录来说吧 , 模拟这个是用户
@Data public class UserDto { private String username; private String password; }
其次就是用户服务
@Service public class UserService { /** * 验证用户密码是否正确 * @param userDto 用户 * @return 验证成功 返回 false */ public boolean verify(UserDto userDto) { // 模拟异常 , 表示次业务线跑不通, 目前还没有写完 throw new RuntimeException(); } }
其次就是我们的Controller层 ,
@Slf4j @RestController @RequiredArgsConstructor @RequestMapping(path = "/v1/user") public class UserController { private final UserService userService; /** * 登录 * @param userDto 用户 * @return 登录成功返回 true */ @PostMapping("/login") public boolean login(UserDto userDto) { // 记录日志 log.info("login username: {}, password :{}.", userDto.getUsername(), userDto.getPassword()); return userService.verify(userDto); } }
此时拿 postman绝对跑不通. 怎么办呢.
主角来了
import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @RunWith(SpringRunner.class) @SpringBootTest // 会自动注入 mockmvc对象 @AutoConfigureMockMvc public class UserControllerTest { @Autowired private MockMvc mockMvc; // 标记service表示无效,走我们的service @MockBean private UserService service; @Test public void findAddr() throws Exception { // 当访问service的 verify方法时. 任何对象都返回true when(service.verify(any(UserDto.class))).thenReturn(true); MvcResult result = mockMvc.perform( post("/v1/user/login") // application/x-www-form-urlencoded , 一般都是选择这个 .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) // 输入参数 .param("username", "root") .param("password", "123456") ) // 打印一些必要的请求信息. .andDo(MockMvcResultHandlers.print()) .andReturn(); String content = result.getResponse().getContentAsString(); Assert.assertEquals(content, "true"); } }
我们最终启动跑一下
2020-04-01 19:56:58.054 INFO 17140 --- [ main] c.e.s.controller.UserController : login username: root, password :123456.
结果完全OK . 是不是很神奇呢.
注意这里的 @MockBean
是Spring提供的. 同时还有, mock官方提供了 @Mock
和 @InjectMocks
, 具体可以看看这篇文章 , 怕使用的时候出错误 , 记录一下 : https://www.jianshu.com/p/c68ee5d08fdd
service层 往往依赖于dao层
所以解决方式也是 , 但是不是web层, 所以往往不需要web环境. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
这个可以关闭web环境.
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public class UserServiceTest { @Autowired UserService service; @Test public void testUser() { service.verify(UserDto.builder().password("111").build()); } }
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public class UserServiceTest { @Autowired private UserService service; @DirtiesContext @Test public void test1() { System.out.println("test-1 " + service.hashCode()); } @DirtiesContext @Test public void test2() { System.out.println("test-2 " + service.hashCode()); } }
我们发现输出的是
test-1 2125470482 test-2 1846539844
这个 @DirtiesContext
有两种
第一种就是方法隔离 .
第二种就是类隔离. 使用场景的话各有差异 ..
测试用例的运行 . 可以直接通过 mvn test 就可以了 , 重定向输出可以用 >> log.log ,这里切记一点. mvn test -Dtest=*Test -DfailIfNoTests=true
, 这个是默认的缺省值. 也就是只会监测到Test后缀的文件进行测试.
其中 @MockBean
可以帮助我们实例化一个mapper对象, 同时我们可以直接调用. 此时就可以直接隔离数据库访问层面.
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) public class UserMapperTest { @MockBean private UserMapper mapper; @Test public void findAddr() { when(mapper.findAddr(anyInt())).thenReturn("北京"); String addr = mapper.findAddr(100); assertEquals("北京", addr); verify(mapper).findAddr(100); } }
加入依赖,
<!-- 方便测试 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.197</version> <scope>test</scope> </dependency>
配置文件的话, 很简单, 我是用的是JPA.
# H2的配置. 就两行其实. 其他我多余加上的. spring.datasource.url=jdbc:h2:mem:jpa;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL spring.datasource.driver-class-name=org.h2.Driver spring.datasource.continue-on-error=false spring.datasource.hikari.minimum-idle=2 # 没次启动创建, 每次结束进程删除 spring.jpa.hibernate.ddl-auto=create-drop # SQL打印开启 spring.jpa.show-sql=true # 数据库引擎修改一下 spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
这个就是一个完整的测试环境. soeasy .
首先呢 , 依赖少不了 .
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency>
其次呢 , 加入配置.
@Configuration @EnableSwagger2 public class SwaggerConfig { /** * 访问接口在 * @return */ @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.springswagger.controller")) .paths(PathSelectors.any()) .build() .apiInfo(apiEndPointsInfo()) .useDefaultResponseMessages(false); } private ApiInfo apiEndPointsInfo() { return new ApiInfoBuilder().title("REST API") .description("Spring Swaager2 REST API") .contact(new Contact("anthony", "https://github.com/Anthony-Dong", "574986060@qq.com")) .license("The Apache License") .licenseUrl("https://opensource.org/licenses/MIT") .version("V1") .build(); } }
其他就直接映射好了
直接访问 http://localhost:8080/swagger-ui.html 既可
比较坑的点 . 第一点, 要写注释就写清楚点像 @ApiParam(value = "用户ID",required = true,example = "1")
, 如果不加 example会抛出异常, 比如 java.lang.NumberFormatException:For input string:""
, 因为我们format的时候失败了. 默认是空. 所以比较坑.
@Slf4j @RestController @RequestMapping(path = "/v1/user") public class UserController { @GetMapping("/info/{id}") @ApiOperation("根据ID获取用户信息") public Map<String, UserDo> userInfo(@ApiParam(value = "用户ID",required = true,example = "1") @PathVariable("id") Long id) { return Collections.singletonMap("user-info", UserDo.builder().userId(id).username("tom").password("123").build()); } }
具体的解决方案就是 如果写 就要加上最好了.
syms x; a = -15; b = -a; step = 0.01; gap = 15; e = 1e-3; while (1) for w= a : step : b s1 = int(exp(-(x-1000)^2/50), -inf, (16-w)/18); s2 = exp(-((16 - w) / 18 - 1000) ^ 2 / 50); u = 5 * sqrt(2 * pi) / s1 + 36 * (16 - w) + 250 + 5 * sqrt(2 * pi) * (w - 5) / s2 * ( - 1 / 18); if abs(u) < gap display(['gap=', num2str(gap), ' w=', num2str(w)]) step = gap / 100; a = w - gap; b = w + gap; gap = gap / 10; break; end end if gap < e break; end end
第一种 使用 , 很方便
public class MockTest { @Test public void test(){ UserMapper mapper = mock(UserMapper.class); UserDo tom = UserDo.builder().userId(1L).username("tom").password("123").build(); when(mapper.findByUserId(anyLong())).thenReturn(tom); // 第一次调用 System.out.println(mapper.findByUserId(1L)); // 第二次调用 System.out.println(mapper.findByUserId(2L)); // 校验 verify(mapper).findByUserId(1L); verify(mapper).findByUserId(2L); } }
第二种 , 更方便
@RunWith(MockitoJUnitRunner.class) public class MockTest { @Mock private UserMapper mapper; @Test public void test(){ UserDo byUserId = mapper.findByUserId(1L); System.out.println(byUserId); } }
第三种 , 一般吧, 其实和第一种相似 .
public class MockTest { @Mock private UserMapper mapper; public MockTest() { mapper = Mockito.mock(UserMapper.class); } @Test public void test(){ UserDo byUserId = mapper.findByUserId(1L); System.out.println(byUserId); } }
第一种就是在配置文件中告诉, 他, 你要启动时注入的脚本
# 必须设置这个.原因可以看 org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer.isEnabled , 像H2是可以不设置的,默认是只有切入式数据库才可以加载启动脚本. spring.datasource.initialization-mode=always # 建表语句 spring.datasource.schema=classpath:schema.sql # 多个的时候可以如下这么写. # spring.datasource.schema[0]=classpath:schema.sql # 插入数据语句 spring.datasource.data=classpath:schema.sql
其中Spring加载的原理是 , 相当的简单. 反正. 因此我写了个脚本. 反正底层原理也是解析SQL语句 , 然后执行. 很简单的. 直接调用它的就可以了.
/** * spring的模式 , 可以执行脚本 , 这个脚本在classpath下面. */ public static void runSql(DataSource dataSource, String... fileInClassPathResources) { if (fileInClassPathResources == null || fileInClassPathResources.length == 0) return; ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); for (String source : fileInClassPathResources) { populator.addScript(new ClassPathResource(source)); } DatabasePopulatorUtils.execute(populator, dataSource); }
利用Mybatis的工具 , 它可以展示SQL. 这个最好了
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency>
执行脚本
public class RunSqlScript { /** * <p>运行指定的sql脚本 * @param sqlStream 需要执行的sql脚本的数据流 */ public static void run(DataSource dataSource, InputStream sqlStream) throws SQLException { // try - with - resource try (Connection conn = dataSource.getConnection()) { // 创建ScriptRunner,用于执行SQL脚本 ScriptRunner runner = new ScriptRunner(conn); runner.setErrorLogWriter(new PrintWriter(System.err)); runner.setLogWriter(new PrintWriter(System.out)); // 执行SQL脚本 runner.runScript(new InputStreamReader(sqlStream)); // 成功就输出成功 System.out.println("load sql script successful"); } } }
SpringBoot整合的话. 可以通过
@SpringBootApplication public class SpringTestApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(SpringTestApplication.class, args); } @Autowired private DataSource dataSource; @Override public void run(String... args) throws Exception { InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("schema.sql"); RunSqlScript.run(dataSource, stream); stream.close(); } }