[TOC]
- Acceptance: Does the whole system work?
- same as: “functional tests,” “customer tests,” “system tests.”, "UI tests"
- 진척도 측정
- Integration: Does our code work against code we can't change?
- spring configuration works ?
- Unit: Do our objects do the right thing, are they convenient to work with?
- iterative discovery of interfaces, design with working code(refactoring)
https://gaboesquivel.com/blog/2014/differences-between-tdd-atdd-and-bdd/
- 구현 전에 사용자, 테스터 및 개발자가 인수 조건(Acceptance Criteria)을 정의하는 협업 실천법
- 모든 프로젝트 구성원이 수행해야 할 작업과 요구 사항을 정확히 이해할 수 있도록 도와줌
- 요구 사항이 만족되지 않으면 테스트가 실패하여 빠른 피드백 제공
- 테스트는 비즈니스 도메인 용어로 기술됨.
- 각 기능은 반드시 실질적이고 측정 가능한 비즈니스 가치를 제공해야합니다.
- 인수 테스트(Acceptance Test)에 요구사항을 명시하는데 촛점. 인수 테스트로 개발을 주도(drive)
https://reqtest.com/testing-blog/tdd-and-atdd-an-overview-of-the-two-popular-methods-2/
- 구현 전에 관련된 모든 사람들(테스터, 개발자 및 사용자)이 한 팀으로써 개발 초기 단계에 시스템이 충족해야하는 인수 조건을 정의하도록하는 공동 작업 방식
https://www.agilealliance.org/glossary/atdd/
- ATDD는 해당 기능을 구현하기 전에 서로 다른 시각을 가진 구성원들(고객, 개발, 테스트)이 인수 테스트를 작성하는 작업 방식
- 고품질의 SW를 작성하는 가장 빠른 방법
- Kent Beck. punch card / print out.
- 작고, 빠른 피드백
- 첫번째 고객. 메뉴얼.
- Write NO production code except to pass a failing test.
- Write only ENOUGH of a test to demonstrate a failure
- Write only ENOUGH production code to pass the test
refactoring: mandatory not optional
- 시간이 없다 ? 별도의 일정 ?
- 화장실 다녀오면서 손 씻을 시간을 별로도 잡나 ???
- Test After도 나쁘지 않지만 그건 TDD(Design)이 아니라 Coverage가 일부 확보된 것
Growing Object-Oriented Software, Guided by Tests
- 특정 클래스에 대한 단위 테스트를 작성함으로써 TDD를 시작하고 싶은 욕구 발생
- 테스트를 전혀 작성하지 않는 것보다는 좋겠지만 단위 테스트만 있는 프로젝트는 TDD 프로세스의 아주 중요한 혜택을 놓치게 됨
- 단위 테스트가 잘 작성된 고품질의 코드들이
- 어디서도 호출되지 않거나
- 시스템의 다른 부분과 통합할 수 없거나
- 재작성해야만 하는
- 경우가 존재한다(bottom-up의 폐해).
- 어디서 코딩을 시작하고, 언제 코딩을 종료할지를 어떻게 알 수 있을까 ?(top-down)
- 특정 기능(Feature)를 구현할 때 우리가 구현하려는 기능을 보여주는 Acceptance Test를 작성함으로써 시작
- 이 테스트가 실패하는 동안은 시스템이 이 기능을 아직 구현하지 못했다는 것을 보여준다. 그리고 테스트가 성공하면 우린 완료한 것이다.
인수테스트 | 단위테스트 |
---|---|
인수테스트 작성으로 기능 구현을 시작 | 객체나 소수의 객체 집합을 격리해서 다룸 |
시스템이 전체적으로 잘 동작하는지 알려줌 | 클래스 설계를 돕고, 동작한다는 확신을 갖게 하는 점에서 중요 |
진척도 측정을 위한 테스트 | 회귀 테스트 |
어디서 시작하고 언제 멈출지 | 빠르게 동작하도록 하고 설계(Refactoring) |
가장 간단한 성공 케이스로 테스트를 시작
- degenerate or failure case로 시작하는 것은 쉽다
- Degenerate cases는 시스템의 가치에 많은 것을 추가하지 않고, 또한 우리의 생각이 유효한지에 대해 충분한 피드백틀 주지 않는다.
- 당신이 읽고 싶은 테스트를 작성하라(Unit vs. Acceptance Test)
사용자(Employee)는 lastName을 인자로 인사말(greeting)을 요구한다. 시스템은 lastName으로 DB에서 Employee를 찾고
- 존재하는 경우 "Hello firstName lastName !"을
- 존재하지 않는 경우 "Who is this lastName you're talking about?"을
반환한다.
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.0.7</version>
<scope>test</scope>
</dependency>
추가
package com.example.employee;
import io.restassured.specification.RequestSpecification;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import static io.restassured.RestAssured.given;
import static org.hamcrest.core.Is.is;
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
classes = EmployeeApplication.class
)
@TestPropertySource(
locations = "classpath:application-integration.properties"
)
public class EmployeeControllerRestAssuredIntegrationTest {
@Autowired
private EmployeeRepository repository;
private RequestSpecification basicRequest;
@Before
public void setUp() {
basicRequest = given()
.baseUri("http://localhost")
.port(8080)
;
repository.deleteAll();
repository.save(new Employee("Baek", "Myeongseok"));
}
@Test
public void shouldReturnDefaultMessageWhenLastNameNotFound() {
String nonExistingLastName = "nonExistingLastName";
String expectedMessage = "Who is this " + nonExistingLastName + " you're talking about?";
given().spec(basicRequest).basePath("/api/hello/" + nonExistingLastName)
.when().get()
.then().log().body()
.statusCode(HttpStatus.OK.value())
.body(is(expectedMessage));
}
@Test
public void shouldReturnGreetingMessageWhenLastNameFound() {
String existingLastName = "Baek";
String expectedMessage = "Hello Myeongseok Baek!";
given().spec(basicRequest).basePath("/api/hello/" + existingLastName)
.when().get()
.then().log().body()
.statusCode(HttpStatus.OK.value())
.body(is(expectedMessage));
}
}
- add src/test/resources/application-integration.properties
spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
- add jpa annotation to Employee, EmployeeRepository
- add EmployeeController
@RestController
@RequestMapping("/api")
public class EmployeeController {
@GetMapping("/hello/{lastName}")
public String greeting(@PathVariable String lastName) {
return "Who is this " + lastName + " you're talking about?";
}
}
controller에서 하드 코딩된 결과를 반환하도록 하여 하나의 테스트는 성공시켰지만 2가지 경우를 성공시키진 못했다. 제대로 하려면 로직이 필요하다.
controller는
- 사용자의 요청 해석(spring-mvc의 경우는 annotation으로 가능)
- controller 수준의 validation
- service로 위임
- 다음 페이지를 결정하고 service에서 반환받은 값을 전달하여 view를 출력
하는 책임을 갖는다(POJOs in Action).
따라서 우리는 service 객체가 필요하다.
@Service
public class GreetingService {
public String greet(String lastName) {
return null;
}
}
로직을 구현할 서비스 객체와 메소드를 발견했으므로 이제부터 TDD로 하나의 메소드를 완성한다.
public class GreetingServiceTest {
private GreetingService greetService;
@Before
public void setUp() throws Exception {
greetService = new GreetingService();
}
@Test
public void greet_with_nonExisting_last_name_should_return_default_message() {
String nonExistingLastName = "nonExistingLastName";
String msg = greetService.greet(nonExistingLastName);
assertThat(msg, is("Who is this " + nonExistingLastName + " you're talking about?"));
}
}
이게 맞으나 이렇게 하면 작은 단위로 빠르게 확인하며 구현하기 어렵다. 왜냐하면 이 테스트를 성공시키기 위해서 service 메소드를 구현해야 하는데 빠르게 한 줄 단위로 실행시키고 확인해 보면서 구현하기 어렵다.
테스트에 직접 서비스 메소드를 구현한다.
이때 실패하는지도 인자를 바꿔서 확인해서 우리의 테스트가 성공하는 경우와 실패하는 경우 모두를 검증하는지 확인
- extract fields
- move up given code to setUp
- declare variables(lastName, employee1, msg1)
- extract method to move
@Service
public class GreetingService {
EmployeeRepository repository;
public String greet(String lastName) {
Optional<Employee> employee = repository.findByLastName(lastName);
return employee
.map(e -> String.format("Hello %s %s!", e.getFirstName(), e.getLastName()))
.orElse("Who is this " + lastName + " you're talking about?");
}
}
move후에 change signature로 test에 대한 의존성 제거
이제 우리의 테스트 코드는 comment out한 초기 의도를 나타낸 테스트 코드와 같아졌다. comment를 삭제하고 , 테스트 명에 맞게 메소드를 분리한다.
controller integration test가 real object로 동작하도록 수정
controller integration test가 제대로된 E2E 테스트(Acceptance)가 되었고, 우리의 작업은 완료되었다.
바로 테스트 코드에서 한줄 단위로 작성/실행/확인하는데...
spring mock mvc도 훌륭. 원격의 CI서버에서 원격의 검증 서버에 대해서 Integration Test를 해야 하는 필요로 rest-assured 선택
@SpringBootTest
- 통합 테스트, 전체 Bean 로딩됨
@WebMvcTest
- WebApplicationContext이 Bean 들이 로딩됨
@DataJpaTest
- Repository 레스트를 위한 JPA 관련 Bean들이 로딩됨
- persistence layer 테스트를 위한 표준 설정 제공
- H2 인메모리 DB, Hiberante, Spring Data, Datasource 등을 설정
- @EntityScan 실행
- SQL 로깅 설정
@ExtendWith(SpringExtension.class)
- spring boot test의 기능과 junit의 연결의 제공
- junit 테스트에서 spring boot test 기능이 필요할 때 이 어노테이션을 사용
@AutoConfigureMockMvc
- MockMvc Autowire 제공
@TestPropertySource
- 테스트에서 사용할 properties 파일의 위치를 설정
- application.properties에 정의된 설정을 오버라이드
TestEntityManager
- TestEntityManager provided by Spring Boot is an alternative to the standard JPA EntityManager that provides methods commonly used when writing tests
@Autowired
private TestEntityManager entityManager;