你会写测试代码吗?

2022-07-08 14:07:45 浏览数 (1)

测试是企业软件开发不可缺少的一部分。

翻开任何一个优秀的开源框架源码,会发现在测试的包里面有不亚于源码的代码量。如何快速的编写出针对性的测试代码,也是一门绝活。

这里不展开讲解Mockito等测试框架,只针对Spring Boot应用,给出Spring Boot开发中常用的测试方法,帮助你进行快速测试开发。

导入依赖


Maven

代码语言:javascript复制
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

Gradle

代码语言:javascript复制
testImplemention "org.springframework.boot:spring-boot-starter-test"

注解


@SpringBootTest : 从当前的标记该注解的测试类开始找,直至找到@SpringBootApplication或者@SpringBootConfiguration。就从标记了上述两个注解的类开始扫描bean。

也就是说,你可以在Test类里面自定义项目启动类。

比如:

下面是你的项目启动入口

代码语言:javascript复制
@SpringBootApplication
@Import(ClassA.class)
public class DemoSpringApplication {

  public static void main(String[] args) {
    new SpringApplication(DemoSpringApplication.class).run(args);
  }
}

如果你的测试类如下

代码语言:javascript复制
@SpringBootTest
public class WhereToScanTest {

  @Test
  void works(@Autowired ApplicationContext applicationContext){
    Assertions.assertThat(applicationContext.getBean(ClassA.class)).isNotNull();
  }
  
}

那么DemoSpringApplication就会是应用启动类,能够测试通过。

但是如果,你在WhereToScanTest该包下创建一个@SpringConfiguration注解的类,只是简单加上一个@SpringBootConfiguration注解,测试就会失败。

代码语言:javascript复制
@SpringBootTest
public class WhereToScanTest {

  @Test
  void works(@Autowired ApplicationContext applicationContext){
    Assertions.assertThat(applicationContext.getBean(ClassA.class)).isNotNull();
  }

  @SpringBootConfiguration
  static class MyConfig{

  }
}

这是因为只扫描标记了@SpringBootConfiguration的MyConfig类下的包,并不会创建ClassA的bean。

使用参数


代码语言:javascript复制
@SpringBootTest(args = "--app.test=true")
public class ArgumentTest {

  @Test
  void argsWorks(@Autowired ApplicationArguments args){
    assertThat(args.getOptionNames().contains("app.test"));
    assertThat(args.getOptionValues("app.test")).containsOnly("true");
  }
}

测试web应用程序


如果只是用@SpringBootTest注解,不会开启web环境,如果想要测试web代码,可以加上@AutoConfigureMockMvc注解。

实例代码:

代码语言:javascript复制
@RestController
public class DemoController {

  @RequestMapping("/hello")
  public String hello(){
    return "Hello from demo";
  }
}
代码语言:javascript复制
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcTest {

  @Test
  void mockMvcWorks(@Autowired MockMvc mockMvc) throws Exception {
    mockMvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string("Hello from demo"));
  }

}

注意:这会扫描所有的spring注解并实例化完整的ApplicationContext,也就是启动整个Spring应用,如果你想只测试mvc部分,可以考虑使用@WebMvcTest。该测试是启动一个mock的web环境。

如果想测试真实的sever,使用如下注解,推荐使用一个随机端口进行测试。

代码语言:javascript复制
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RunningServerTest {

  private WebTestClient webTestClient;

  @LocalServerPort
  private int port;

  private String url;

  @BeforeEach
  void before(){
    this.url = "http://localhost:"   port;
    this.webTestClient = WebTestClient.bindToServer().responseTimeout(Duration.ofSeconds(10)).baseUrl(url).build();
  }

  @Test
  void runningServerWorks(@Autowired WebTestClient webTestClient){
    this.webTestClient.get().uri("/hello").exchange().expectStatus().isOk().expectBody().equals("Hello from demo");
  }
}

其中@LocalServerPort会将程序启动的端口注入到port字段里去。

Mock


适合哪种情况?某些服务在开发环境无法调用,那么就需要mock,mock意思是模拟,也就是说模拟某些bean来进行你想要的测试。

例如你定义了一个远程访问的service,但是开发环境无法调通,则可以模拟。

@MockBean的用法

代码语言:javascript复制
@SpringBootTest
public class MockBeanTests {

  @MockBean
  private RemoterService remoterService;

  @Test
  void mockBeanWorks(){
    //如果调用sayHello方法,返回Hello mock bean
    given(remoterService.sayHello()).willReturn("Hello mock bean");
    String s = this.remoterService.sayHello();
    assertThat(s).isEqualTo("Hello mock bean");
  }
}

@MockBean 向测试程序注入了一个RemoteService的Bean,但是具体怎么定义方法怎么执行是需要你来说明的。其中given()方法是Mockito测试框架的方法,意思是如果调用remoteService的sayHello方法,就返回“Hello mock bean”。

分模块测试(WebMVC)


如果使用SpringBootTest,就是扫描整个应用内的bean。在一个项目中可能有很多的Spring Boot Starter,例如只想测试mvc,而不想测试jdbc,那么就需要使用@...Test。

使用@WebMvcTest注解,只会自动配置webmvc相关的功能,只会扫描如下的Bean。

@Controller

@ControllerAdvice

@JsonComponent

Converter

Filter

WebMvcConfigure

...

指定只测试某个Controller:

代码语言:javascript复制
@WebMvcTest(UserController.class)
public class MockMvcTest {

  @MockBean
  private UserService userService;

  @Autowired
  private MockMvc mvc;

  @Test
  void mockMvcWorks(@Autowired MockMvc mockMvc) throws Exception {
    mockMvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string("Hello from demo"));
  }

  @Test
  void mcvWorks() throws Exception{
    given(userService.getUserName()).willReturn("Tyler");
    this.mvc.perform(get("/user/name")).andExpect(status().isOk()).andExpect(content().string("Tyler"));
  }

}

@WebMvcTest(xxxController.class) 只向web中添加该controller,例如该例子只会有UserController,如果还有其他Controller定义其他的@RequestMapping,在测试程序中访问是会404,因为这里我们只定义加载了UserController。

分模块测试(Data JPA )


和上面的mvc模块一样,@DataJpaTest也是只开启JPA相关自动配置,只扫描@Entinty和JpaRepository。使用@DataJpaTest在会回退事务,所以不用担心会向数据库插入无效的数据,默认该注解会使用内嵌的内存数据库,如果想要使用你本地的例如localshot:3306数据库,需要使用如下注解。

可以注入TestEntityManager进行一些操作,也可以注入测试自定义的Repository。

代码语言:javascript复制
@AutoConfigureTestDatabase(replace = Replace.NONE)
代码语言:javascript复制
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class MyRepositoryTests {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository repository;

    @Test
    void testExample() throws Exception {
        this.entityManager.persist(new User("sboot", "1234"));
        User user = this.repository.findByUsername("sboot");
        assertThat(user.getUsername()).isEqualTo("sboot");
        assertThat(user.getEmployeeNumber()).isEqualTo("1234");
    }

}

Spring其他测试方法


如果你什么注解也不想用,既不想测试Data JPA 也不想测试 mvc,只是想注册几个bean,然后启动做些测试,那么也可以用下面两个类。

可以用ApplicationContextRunner,该类是一个标准的,无web的环境。

可以直接用ApplicationContext,该类是Spring为应用程序提供配置的核心接口,例如AnnotationConfigApplicationContext。

用法如下:

代码语言:javascript复制
public class ApplicationContextRunnerTests {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

  @Test
  void works(){
    contextRunner.withBean("classa",ClassA.class).run((context -> {
      ClassA bean = context.getBean(ClassA.class);
      assertThat(context).hasBean("classA");
    }));
  }
}
代码语言:javascript复制
public class AnnotationConfigTests {

  private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

  @Test
  void works(){
    this.context.register(ClassA.class);
    this.context.refresh();
    Assertions.assertThat(this.context.containsBean("classA")).isTrue();
  }
}

总结


能够写出有针对性的测试代码,其实也不是一件容易的事,如果你对代码质量有较高要求,代码层面测试是不可缺少的一部分。希望这篇文章能帮到你一二。这里只是大概列出了一些测试案例,养成代码测试的习惯,更多测试的技巧可以在不断的测试中自己挖掘。

0 人点赞