본문 바로가기

Clean Code

단위 테스트 #8

목차

  • 테스트 코드의 중요성
  • 테스트의 종류
  • Unit Test 작성

테스트 코드의 중요성

  • 테스트 코드는 실수를 바로 잡아준다.
  • 테스트 코드는 반드시 존재해야하며, 실제 코드 못지 않게 중요하다.
  • 테스트 케이스는 변경이 쉽도록 한다. 코드에 유선성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위테스트다. 테스트 케이스가 있으면 변경이 두렵지 않다. 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다. 테스트 커버리지가 높을수록 버그에 대한 공포가 줄어든다.
  • 지저분한 테스트 코드는 테스트를 안하니만 못하다.

테스트는 자동화되어야 한다.

테스트를 양심껏 가끔하는 것이 아니라 매번 배포하기전에 테스트를 실행하여 잘 동작하는지 검증하여야 한다.

 

테스트 의 종류

Test Pyramid

70% Unit tests, 20% Integration tests, 10% end-to-end tests

 

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

Unit test 작성법

테스트 라이브러리 

  • JUnit 
  • Mockito

Test Double - 테스트에서 원본 객체를 대신하는 개체

Stub

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

Spy

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

Mock

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

given-when-then 패턴을 사용하자

  • given: 테스트에 대한 pre-condition
  • when: 테스트 하고 싶은 동작 호출
  • then : 테스트 결과 확인

Spring Boot Application Test  예제

ExampleController 

@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));
    }
}

ExampleController  - Unit test

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?"));
    }
}
  • PersonRepository mock 사용
  • given-when-then 구조
  • repository 에서 값을 가져왔을 때와 읽어오지 못했을 때 2가지 경우를 테스트

Integration Test (Database)

@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)));
    }
}

Integration Test (Service)

@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 : 서버에 응답을 설정한다
        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' 카테고리의 다른 글

관심사 분리 패턴 #10  (0) 2021.08.17
클래스를 잘 설계하기 #9  (0) 2021.08.09
모호한 경계를 구분짓기 #7  (0) 2021.08.05
예외 처리하기 #6  (0) 2021.08.04
객체와 자료구조 #5  (0) 2021.08.04