티스토리 뷰

목차

1. 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

2. 깨끗한 테스트 코드

3. FIRST 규칙

4. TDD 법칙

5. 테스트 라이브러리 사용

6. Test Double

7. 테스트 종류

8. 예제

 

 

 

 

1. 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

테스트 케이스가 없다면 모든 변경이 잠정적인 버그가 된다. 따라서 개발자는 변경에 주저하게 된다.

테스트 케이스가 있으면 부실하거나 엉망인 코드라도 변경하며 개선해 나갈 수 있다.

그러므로 실제 코드를 점검하는 자동화된 단위 테스트가 필요하고 테스트 코드를 깨끗하게 유지해야 한다.

 

2. 깨끗한 테스트 코드

지저분한 테스트 코드는 테스트를 안하니만 못하다.

깨끗한 테스트 코드를 만들기 위해선 가독성이 중요하다.

명료성, 단순성, 풍부한 표현력이 필요하다.

 

리팩토링 전)

public void testGetPageHieratchyAsXml() throws Exception
{
    crawler.addPage(root, PathParser.parse("PageOne"));
    crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
    crawler.addPage(root, PathParser.parse("PageTwo"));
    request.setResource("root");
    request.addInput("type", "pages");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response =
            (SimpleResponse) responder.makeResponse(
                    new FitNesseContext(root), request);
    String xml = response.getContent();
    assertEquals("text/xml", response.getContentType());
    assertSubString("<name>PageOne</name>", xml);
    assertSubString("<name>PageTwo</name>", xml);
    assertSubString("<name>ChildOne</name>", xml);
}

 

1) given-when-then 패턴을 사용하자.

given : 테스트 자료를 만든다.

when : 테스트 자료를 조작한다.

then : 조작한 결과가 올바른지 확인한다.

 

2) 테스트 API를 만들어 사용하자.

기존의 테스트 API를 사용하는 대신 그 위에 함수와 유틸리티를 구현해 API를 만들어 사용하면

테스트 코드를 짜기도 읽기도 쉬워진다.

 

리팩토링 후)

public void testGetPageHierarchyAsXml() throws Exception {
	//given
    makePages("PageOne", "PageOne.ChildOne", "PageTwo");
    
    //when
    submitRequest("root", "type:pages");
    
    //then
    assertResponseIsXML();
    assertResponseContains(
            "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
    );
}

 

3) 테스트당 assert 를 최대한 줄이자.

assert 문이 하나인 함수는 코드를 이해하기 쉬워진다.

여러개의 assert를 수행하는 테스트는 함수를 쪼개면 가능하지만

중복되는 코드가 발생할 수 있다.

 

복합적으로 고려해서 테스트당 assert 를 최대한 줄이자.

 

3. FIRST 규칙

Fast 빠르게 : 테스트는 빨라야 한다.

Inenpendent 독립적으로 : 각 테스트는 서로 의존하면 안된다. (의존적이면 실패 원인을 찾기 어렵다.)

Repeatable 반복가능하게 : 환경과 무관하게 반복 실행 되어야 한다.

Self-Validating 자가검증하는 : 테스트 스스로 성공과 실패를 가늠하도록 만든다.

                                      (테스트 결과 확인을 위해 로그를 확인하도록)

Timely 적시에 : 프로덕트 코드를 구현하기 직전에 구현한다. (TDD 법칙)

 

4. TDD 법칙

첫째 : 실패하는 단위 테스트를 작성한다.

둘째 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 프로덕트 코드를 작성한다.

셋째 : 현재 실패하는 테스트를 통과할 정도로만 프로덕트 코드를 작성한다.

 

 

5. 테스트 라이브러리 사용

JUnit 5, AssertJ, Mockito : 링크 참고

 

6. Test Double

테스트에서 원본 객체를 대신하는 객체

 

Stub
원래의구현을최대한단순한것으로대체한다
테스트를위해프로그래밍된항목에만응답한다


Spy
Stub의역할을하면서호출에대한정보를기록한다
이메일서비스에서메시지가몇번전송되었는지확인할때


Mock
행위를검증하기위해가짜객체를만들어테스트하는방법
호출에대한동작을프로그래밍할수있다.
Stub은상태를검증하고Mock은행위를검증한다.

 

7. 테스트 종류

  • UnitTest:프로그램내부의개별컴포넌트의동작을테스트한
    다.배포하기전에자동으로실행되도록많이사용한다.
  • IntegrationTest:프로그램내부의개별컴포넌트들을합쳐서
    동작을테스트한다.UnitTest는각컴포넌트를고립시켜테스트
    하기때문에컴포넌트의interaction을확인하는Integration
    Test가필요하다.
  • E2ETest:EndtoEndTest.실제유저의시나리오대로네트워
    크를통해서버의Endpoint를호출해테스트한다

 

8. 예제

1) Unit Test

Controller

personRepository 객체를 통해 Person 객체를 조회한다.

findByLastName() 으로 객체를 조회했을 때

값이 있으면 이름으로 문자열을 만들어 반환하고, 없으면 안내 메세지를 리턴한다.

@RestController
public class ExampleController {

    private final PersonRepository personRepo;

    @Autowired
    public ExampleController(final PersonRepository personRepo) {
        this.personRepo = personRepo;
    }

    @GetMapping("/hello/{lastName}")
    public String hello(@PathVariable final String lastName) {
        Optional<Person> foundPerson = personRepo.findByLastName(lastName);

        return foundPerson
                .map(person -> String.format("Hello %s %s!",
                        person.getFirstName(),
                        person.getLastName()))
                .orElse(String.format("Who is this '%s' you're talking about?",
                        lastName));
    }
}

Unit Test

PersonRepository mock 을 만들어 사용한다.

given-when-then 구조이고, 컨트롤러의 반환값 두 가지 경우를 테스트 한다.

public class ExampleControllerTest {

    private ExampleController subject;

    @Mock
    private PersonRepository personRepo;

    @Before
    public void setUp() throws Exception {
        initMocks(this);
        subject = new ExampleController(personRepo);
    }

    @Test
    public void shouldReturnFullNameOfAPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        given(personRepo.findByLastName("Pan"))
            .willReturn(Optional.of(peter));

        String greeting = subject.hello("Pan");

        assertThat(greeting, is("Hello Peter Pan!"));
    }

    @Test
    public void shouldTellIfPersonIsUnknown() throws Exception {
        given(personRepo.findByLastName(anyString()))
            .willReturn(Optional.empty());

        String greeting = subject.hello("Pan");

        assertThat(greeting, is("Who is this 'Pan' you're talking about?"));
    }
}

2) Integration Test

(1) Integration Test (Database)

Integration Test (Database)

PersonRepository 가 데이터베이스와 정상적으로 연결 동작하는 지 확인

테스트 간의 의존성이 생기지 않도록 테스트 실행 전에 tearDown() 에서 테이블의 데이터를 삭제한다.

findByLastName() 이 정상 동작하는지 확인

@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryIntegrationTest {
    @Autowired
    private PersonRepository subject;

    @After
    public void tearDown() throws Exception {
        subject.deleteAll();
    }

    @Test
    public void shouldSaveAndFetchPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        subject.save(peter);

        Optional<Person> maybePeter = subject.findByLastName("Pan");

        assertThat(maybePeter, is(Optional.of(peter)));
    }
}

 

(2) Integration Test (Service)

Integration Test (Service)

WireMock을 이용해 mock 서버를 띄운다.

client가 실제 서버가 아닌 mock 서버로 요청하게 해서 client 동작을 테스트한다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientIntegrationTest {

    @Autowired
    private WeatherClient subject;

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8089); // mock 서버를 띄운다.

    @Test
    public void shouldCallWeatherService() throws Exception {
    	// given: mock 서버에 응답을 설정한다.
        wireMockRule.stubFor(get(urlPathEqualTo("/some-test-api-key/53.5511,9.9937"))
                .willReturn(aResponse()
                        .withBody(FileLoader.read("classpath:weatherApiResponse.json"))
                        .withHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withStatus(200)));
		
        // when: weatherClient가 mock서버로부터 응답을 받는다.
        Optional<WeatherResponse> weatherResponse = subject.fetchWeather();
		
        // then: 받아온 응답이 기대값과 일치하는 지 확인한다.
        Optional<WeatherResponse> expectedResponse = Optional.of(new WeatherResponse("Rain"));
        assertThat(weatherResponse, is(expectedResponse));
    }
}

 

 

 

 

 

 

출처

Clean Code(클린 코드) 애자일 소프트웨어 장인 정신 - 로버트 C. 마틴 지음

https://zero-base.co.kr/category_dev_camp/cleancode_1book

728x90