美文网首页
2020-10-26 Spring Boot 的TDD使用方法

2020-10-26 Spring Boot 的TDD使用方法

作者: opengladv | 来源:发表于2020-10-26 14:30 被阅读0次

    Spring Boot 的TDD使用方法

    来源

    • Spring boot 网站分享的链接 https://content.pivotal.io/springone-platform-2017/test-driven-development-with-spring-boot-sannidhi-jalukar-madhura-bhave
    • 代码链接github.com/mbhave/tdd-with-spring-boot
    • 代码链接2github.com/sannidhi/tdd-boot-demo

    基本内容

    • TDD 测试驱动开发,是先写测试,之后再补充代码。
    • 主要的困难在于写测试的时候很多类都没有。
    • 解决方案是:通过Mock的形式完成测试条件的准备,之后再补全对应的业务代码。
    • 测试分为3种
      • integrationTest,集成测试,测试真实的业务逻辑。
      • UnitTest + Spring,使用Spring的AutoWire等相关资源的单元测试。
      • UnitTest,单独的一个类的测试。

    IntegrationTest

    
    ////////// IntegrationTests.java //////////
    
    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class IntegrationTests {
    
        @Autowired
        private TestRestTemplate testRestTemplate;
    
        @Test
        public void getCar_WithName_ReturnsCar() {
            ResponseEntity<Car> responseEntity = this.testRestTemplate.getForEntity("/cars/{name}", Car.class, "prius");
            Car car = responseEntity.getBody();
            assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
            assertThat(car.getName()).isEqualTo("prius");
            assertThat(car.getType()).isEqualTo("hybrid");
        }
    }
    
    ////////// Car.java //////////
    
    @Entity
    public class Car {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String name;
    
        private String type;
    
        public Car(String name, String type) {
            this.name = name;
            this.type = type;
        }
    
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getType() {
            return this.type;
        }
    
        public void setType(String type) {
            this.type = type;
        }
    }
    
    • 主要使用了@SpringBootTest注解,并指定了RandomPort
    • 请求通过TestRestTemplate构建,指定对应的数据类型,自动转换。
    • 每个结果通过assertThat进行验证
    • 集成测试用于验证整个的业务流程是否正确。并没有Mock的部分。是实际的程序运行测试。
    • 需要的主要是Entity对象,或者是JSON对象
    • 需要启动整个Spring程序以及tomcat服务器进行测试

    ControllerTest

    
    ////////// CarsControllerTests.java //////////
    
    @RunWith(SpringRunner.class)
    @WebMvcTest
    public class CarsControllerTests {
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        private CarService carService;
    
        @Test
        public void getCar_WithName_ReturnsCar() throws Exception {
            when(this.carService.getCarDetails("prius")).thenReturn(new Car("prius", "hybrid"));
            this.mockMvc.perform(get("/cars/{name}", "prius"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("name").value("prius"))
                    .andExpect(jsonPath("type").value("hybrid"));
        }
    
        @Test
        public void getCar_NotFound_Returns404() throws Exception {
            when(this.carService.getCarDetails(any())).thenReturn(null);
            this.mockMvc.perform(get("/cars/{name}", "prius"))
                    .andExpect(status().isNotFound());
        }
    
    }
    
    ////////// CarsController.java //////////
    
    @RestController
    public class CarsController {
    
        private final CarService carService;
    
        public CarsController(CarService carService) {
            this.carService = carService;
        }
    
        @GetMapping("/cars/{name}")
        public Car getCar(@PathVariable String name) {
            Car car = this.carService.getCarDetails(name);
            if (car == null) {
                throw new CarNotFoundException();
            }
            return car;
        }
        
    //  @ExceptionHandler
    //  @ResponseStatus(HttpStatus.NOT_FOUND)
    //  private void carNotFoundHandler(CarNotFoundException ex){}
    
    }
    
    ////////// CarService.java //////////
    
    @Service
    public class CarService {
    
        public Car getCarDetails(String name) {
            return null;
        }
    }
    
    ////////// CarNotFoundException.java //////////
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public class CarNotFoundException extends RuntimeException {
    }
    
    
    
    
    • 接口api测试,属于UnitTest+Spring
    • 使用@WebMvcTest注解
    • 使用了MockMvc模拟请求
    • 使用了MockBean作为模拟Service的返回值,发送给Controller
    • Controller调用Service的方式可以是直接的构造函数的参数包含。
    • 此时Service的逻辑不需要实现。因为都通过Mock实现了。
    • 因此只需要验证Controller的逻辑即可。
    • 对于Exception,优先使用RuntimeException()
    • 当Exception需要转换为不同的Response的时候
      • 可以使用@ResponseStatus,放在对应的Exception类上
      • 或者使用ExceptionHandler处理,也需要增加@ResponseStatus注解到对应方法上。

    ServiceTest

    
    ////////// CarServiceTest.java //////////
    @RunWith(MockitoJUnitRunner.class)
    public class CarServiceTest {
    
        @Mock
        private CarRepository carRepository;
    
        private CarService carService;
    
        @Before
        public void setUp() throws Exception {
            carService = new CarService(carRepository);
        }
    
        @Test
        public void getCarDetails_returnsCarInfo() {
            given(carRepository.findByName("prius")).willReturn(new Car("prius", "hybrid"));
    
            Car car = carService.getCarDetails("prius");
    
            assertThat(car.getName()).isEqualTo("prius");
            assertThat(car.getType()).isEqualTo("hybrid");
        }
    
        @Test(expected = CarNotFoundException.class)
        public void getCarDetails_whenCarNotFound() throws Exception {
            given(carRepository.findByName("prius")).willReturn(null);
    
            carService.getCarDetails("prius");
        }
    }
    
    ////////// CarService.java //////////
    @Service
    public class CarService {
    
        private CarRepository carRepository;
    
        public CarService(CarRepository carRepository) {
            this.carRepository = carRepository;
        }
    
        public Car getCarDetails(String name) {
            Car car = carRepository.findByName(name);
            if(car == null) {
                throw new CarNotFoundException();
            }
            return car;
        }
    }
    
    ////////// CarRepository.java //////////
    public class CarRepository{
        Car findByName(String name){
            return null;
        }
    }
    
    
    • 服务测试,属于UnitTest
    • 仅仅验证Service的业务逻辑,使用MockitoJUnitRunner运行,而不是SpringRunner
    • 也就是不会使用AutoWire的功能,所以Repository需要通过构造函数传入。
    • 使用@Mock注解模拟Repository的功能。
    • 使用Mokito的given方法仿真Repository的对应方法的返回值。
    • 此时CarRepository不需要对方法进行实现也可以有返回值。
    • 直接使用service对象调用对应方法进行测试。

    RepositoryTest

    ////////// CarRepositoryTest.java //////////
    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class CarRepositoryTests {
    
        @Autowired
        private CarRepository repository;
    
        @Autowired
        private TestEntityManager testEntityManager;
    
        @Test
        public void findByName_ReturnsCar() throws Exception {
            Car savedCar = testEntityManager.persistFlushFind(new Car("prius", "hybrid"));
            Car car = this.repository.findByName("prius");
            assertThat(car.getName()).isEqualTo(savedCar.getName());
            assertThat(car.getType()).isEqualTo(savedCar.getType());
        }
    }
    
    ////////// CarRepository.java //////////
    public interface CarRepository extends CrudRepository<Car,String> {
        Car findByName(String name);
    }
    
    • 资源测试,属于UnitTest+Spring
    • 由于需要使用到实际的数据库,所以需要调用Spring。
    • 使用DataJpaTest注解,引用进行Repository测试的相关资源。其数据库默认是内部的内存数据库。
    • TestEntityManager可以直接使用。其中的persistFlushFind方法可以避免Repository.save的使用,为数据库添加一些临时资源,并自动回滚删除。
    • 如果不使用TestEntityManager,则需要在测试前首先配置数据库数据,然后手动delete。
    • Repository测试主要就是用来测试SQL语句是否符合逻辑。
    • 同时DataJpaTest可以使用@AutoConfigureTestDatabase指定自己需要的数据库。

    CachingTest

    ////////// CachingTest.java //////////
    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
    @AutoConfigureTestDatabase
    public class CachingTest {
    
        @Autowired
        private CarService service;
    
        @MockBean
        private CarRepository repository;
    
        @Test
        public void getCar_ReturnsCachedValue() throws Exception {
            given(repository.findByName(anyString())).willReturn(new Car("prius", "hybrid"));
            service.getCarDetails("prius");
            service.getCarDetails("prius");
            verify(repository, times(1)).findByName("prius");
        }
    }
    
    ////////// CarsApplication.java //////////
    @SpringBootApplication
    @EnableCaching
    public class CarsApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CarsApplication.class, args);
        }
    }
    
    ////////// CarService.java //////////
    @Service
    public class CarService {
    
        private CarRepository carRepository;
    
        public CarService(CarRepository carRepository) {
            this.carRepository = carRepository;
        }
    
        @Cacheable("cars")
        public Car getCarDetails(String name) {
            Car car = carRepository.findByName(name);
            if(car == null) {
                throw new CarNotFoundException();
            }
            return car;
        }
    }
    
    ////////// CarRepository.java //////////
    public class CarRepository{
        Car findByName(String name){
            return null;
        }
    }
    
    
    
    • 缓存测试,主要用来测试缓存的请求情况
    • 不需要使用网络环境webEnvironment = SpringBootTest.WebEnvironment.NONE
    • 使用内存数据库@AutoConfigureTestDatabase
    • 需要在Application上使用@EnableCaching启动缓存
    • 同时对应的Service方法需要进行@Cacheable("cars")注释,标记这返回值需要缓存。
    • 最后在测试的时候,多次请求service的对应方法,并检查Mock的Repository对应的方法调用次数,来判定是否使用了缓存的数据。

    相关文章

      网友评论

          本文标题:2020-10-26 Spring Boot 的TDD使用方法

          本文链接:https://www.haomeiwen.com/subject/ckmamktx.html