From f16d8a4964b71d2f60daf671b8bfdc13d95dcf03 Mon Sep 17 00:00:00 2001 From: Yeshin Lee Date: Sat, 16 Nov 2024 13:28:22 +0900 Subject: [PATCH] Chapter 6: Testing --- ...4_\354\247\200\354\233\220\352\265\260.md" | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 "The_Missing_Readme/6\354\236\245_\355\205\214\354\212\244\355\212\270!_\352\260\234\353\260\234\354\236\220\354\235\230_\353\223\240\353\223\240\355\225\234_\354\247\200\354\233\220\352\265\260.md" diff --git "a/The_Missing_Readme/6\354\236\245_\355\205\214\354\212\244\355\212\270!_\352\260\234\353\260\234\354\236\220\354\235\230_\353\223\240\353\223\240\355\225\234_\354\247\200\354\233\220\352\265\260.md" "b/The_Missing_Readme/6\354\236\245_\355\205\214\354\212\244\355\212\270!_\352\260\234\353\260\234\354\236\220\354\235\230_\353\223\240\353\223\240\355\225\234_\354\247\200\354\233\220\352\265\260.md" new file mode 100644 index 0000000..aeb3c20 --- /dev/null +++ "b/The_Missing_Readme/6\354\236\245_\355\205\214\354\212\244\355\212\270!_\352\260\234\353\260\234\354\236\220\354\235\230_\353\223\240\353\223\240\355\225\234_\354\247\200\354\233\220\352\265\260.md" @@ -0,0 +1,303 @@ +# 6장 테스트! 개발자의 든든한 지원군 + +- 테스트를 작성하고 실행하고 수정하는 일은 쓸모없는 작업처럼 보일 수 있다. + +- 좋지 않은 테스트는 아무런 효과없이 개발자의 업무 부하만 가중시킬 수 있다. + +## 테스트를 꼭 해야 할까 + +- 대부분의 개발자는 테스트란 코드의 동작을 확인하는 것임을 잘 알고 있지만 그 것이 테스트의 목적은 아니다. + +- 테스트의 목적은 나중에 의도치 않게 코드의 변경이 바뀌는 것을 방지하고 깔끔한 코드를 작성하게 도와준다. + +- 테스트는 소프트웨어가 원하는 대로 동작하는지를 검증하고 프로그램의 인터페이스와 실제 구현에 대해 고민해볼 수 있다. + +- 오래된 테스트가 실패하면 개발자가 동작을 의도적으로 변경한건지 버그가 발생한건지 판단해야 한다. + +- TDD(Test-Driven Development) 원칙을 따르게 되면, 코드 작성 전 코드 동작이나 인터페이스 설계, 다른 코드와의 통합 등을 먼저 고려하게 된다. + +- 경험이 많은 프로그래머는 새로운 코드베이스를 만날 경우 코드를 이해하기 위해 가장 먼저 읽는 것이 테스트 코드다. + +- 테스트에 디버거를 붙여 코드를 한 줄 한 줄 실행해 볼 수 있다. + +## 테스트의 유형과 기법 + +- 단위 테스트(unit test): 코드의 '단위', 즉 메서드나 동작 하나를 검증한다. 빠르고 작으며 집중적이여야 한다. + +- 통합 테스트(integration test): 여러 컴포넌트가 함께 어우러져 동작하는지 검증한다. 단위 테스트보다 느리며 정교한 셋업이 필요하며 피드백 루프가 길다. + +- 시스템 테스트(system test): 시스템 전체를 검증하며 종단 간(end-to-end; e2e) 워크플로우는 프로덕션 환경의 전반적인 단계에서 실제 사용자의 동작을 시뮬레이션한다. + +- 성능 테스트(performance test): 주어진 설정하에 시스템의 성능을 측정하여 용량 계획과 SLO(service-level objective; 서비스 수준 목표) 설정에 도움된다. + + - 부하 테스트(load test): 시스템에 다양한 수준의 부하를 걸고 성능을 측정한다. + + - 스트레스 테스트(stress test): 장애가 발생하는 수준까지 부하를 올려 부하가 걸릴 시 어떤 일이 벌어지는지 확인한다. + +- 인수 테스트(acceptance test): 공식 인수 테스트 및 조건이 계약에 포함된 엔터프라이즈 소프트웨어 대상으로 인수 조건에 만족하는지 검증하는 테스트로 ISO(International Standards Organization; 국제 표준화 기구) 보안 표준의 일부로서 요구한다. + +## 테스트 도구 + +- 모킹(mocking) 라이브러리 같은 테스트 작성 도구를 이용하면 깔끔하고 효율적인 테스트를 작성할 수 있다. + +- 테스트 프레임워크는 셋업부터 마무리까지 테스트의 수명주기를 모델링해서 테스트를 실행하게 해주는 도구다. + +- 코드 품질 도구는 빌드나 컴파일 단계에서 코드 커버리지(coverage)와 복잡도 등을 분석하거나 정적 분석을 통한 버그 발견, 코딩 스타일 에러 감사 등에 사용한다. + +- 복잡도에 대한 트레이드오프를 정당화할 수 있으면서 팀이 동의하기 전까지는 잘 알려지지 않은 도구를 도입하는 것은 지양하는 것이 좋다. + +### 모킹 라이브러리 + +- 객체지향 코드의 단위 테스트에 주로 사용한다. + +- 모의 객체(mock)은 입력에 대해 하드코딩된 응답을 리턴함으로써 테스트에 필요한 기능을 구현한다. + +- 외부 의존성을 줄이면 단위 테스트를 더 빠르고 집중적으로 수행할 수 있다. + +- 셋업이 간편해지고 테스트가 네트워크 호출을 직접 만들지 않아도 되면서 작업이 느려지는 것을 피할 수 있다. + +- 모의 객체에 복잡한 로직을 넣으면 테스트가 불안정해지고 난해해진다. + +- 모의 객체가 필요한 시점이 되면 모의 시스템에 대한 의존성을 제거하도록 리팩토링이 가능한지부터 고민해보자. + +### 테스트 프레임워크 + +- 테스트 코드를 작성하고 실행하는 도구로 다음 역할을 수행한다. + + - 테스트 셋업과 해제 + + - 테스트 실행 및 조율(orchestration): 테스트를 격리하면서 속도를 조정할 수 있으면서 직렬 또는 병렬로 실행할 수 있다 + + - 테스트 결과 보고서 생성: 개발자가 실패한 빌드를 디버깅하는데 도움이 되지만 테스트 결과의 저장 위치가 명확하지 않을 수 있다 + + - 추가 검증 메서드 등의 도구 제공 + + - 코드 커버리지 도구와의 통합 + +### 코드 품질 도구 + +- 린터(linter)는 코드 품질 규칙을 강제하는 도구로 정적 분석과 스타일 검사를 실행한다. + +- 정적 코드 분석기(static code analyzer)는 문법 오류를 검사하는 컴파일러가 없는 동적 언어(python, javascript 등)를 사용할 때 중요하다. + +- 분석기는 코드 악취나 의심스러운 코드를 강조하는 기능을 제공하고 코드 애노테이션을 통해 [거짓 양성(false positive)](https://ko.wikipedia.org/wiki/%EA%B1%B0%EC%A7%93_%EC%96%91%EC%84%B1%EA%B3%BC_%EA%B1%B0%EC%A7%93_%EC%9D%8C%EC%84%B1) 보고의 가능성을 줄여야 한다. + +- 코드 스타일 검사기(code style analyzer)를 사용하면 모든 소스 코드를 동일한 방식으로 작성하여 일관성을 유지할 수 있다. + +- 코드 복잡도 도구(code complexity tool)는 순환 복잡도(cyclomatic complexity)를 계산하거나 코드의 분기 횟수로 지나치게 복잡한 로직을 작성하지 않게 한다. + +- 코드 커버리지 도구(code coverage tool)는 테스트가 몇 줄의 코드를 실행했는지를 측정하는 도구로 적절한 수준(65%~85%)을 유지하자. + +- 코드 품질 도구가 찾은 품질 이슈가 반드시 실질적인 문제라는 보장이 없으므로 코드 품질 지표에 집착하지 말자. + +## 개발자 스스로 직접 테스트를 작성하자 + +- QA(quality assurance)팀은 작성한 코드가 의도대로 동작하게 만드는 역할 외에 다음 역할을 맡고 있다. + + - 블랙박스(black-box) 또는 화이트박스(white-box) 테스트 작성 + + - 성능 테스트 작성 + + - 통합 테스트, 사용자 인수 테스트, 시스템 테스트 수행 + + - 테스트 도구 제공 및 유지 보수 + + - 테스트 환경과 인프라(infrastructure) 유지 보수 + + - 정식 테스트 인증과 릴리즈 절차 정의 + +- QA팀이 모든 테스트를 수행하게 해서는 안되며 협업하는 방법에 대해 알아야 한다. + +### 테스트는 깔끔하게 작성하자 + +- 테스트도 코드의 일부므로 유지보수가 필요하고 시간이 지나면 리팩토링도 해야 한다. + +- 편법으로 작성한 테스트는 안정성을 떨어트리고 유지보수 비용을 높여 향후 개발 속도를 저해하는 요인이 될 수 있다. + +- 소프트웨어 설계 권장 기법을 적용해 테스트의 응집력을 유지하면서 결합력을 낮추자. + +- 상세 구현보다 근본적인 기능을 테스트하는데 중점을 두어야 한다. + +### 과도한 테스트는 삼가자 + +- 테스트는 실패했을 때 충분한 의미를 갖도록 작성하자. + +- 코드에 내포된 위험에 가장 큰 영향을 미치는 테스트를 작성하는데 주력하자. + +- 코드 커버리지는 높다고 코드가 옳은 것을 보장하지 않으므로 가이드 정도로 염두해두자. + +- 자동으로 생성되는 코드에 대한 테스트는 하지 말자. + +- 에러 발생 가능성이 높고 영향도가 큰, 즉 가장 가치있는 테스트에 집중하는 것이 비용대비 효과가 크므로 [위험 매트릭스(risk matrix)](https://en.wikipedia.org/wiki/Risk_matrix)를 이용해보자. + +## 테스트 결정성: 항상 동일한 테스트 결과를 만드려면 + +- 결정적 코드(deterministic code)란 입력이 같으면 출력이 항상 동일한 코드를 의미한다. + +- 네트워크 소켓을 통해 원격 웹서비스를 호출하는 단위 테스트 등 비결정적 코드(nondeterministic code)는 재현과 디버깅이 어려우므로 테스트의 가치를 떨어트린다. + +- 간헐적으로 실패하는 테스트가 발생할 경우 즉시 수정하거나 비활성화시켜야 한다. + +- 간헐적으로 실패하는 경우, 테스트 간의 간섭이나 특정 머신 설정에 의해 발생할 수도 있으므로 다방면으로 실험해봐야 한다. + +### 난수 생성기에 적절한 시드값을 사용하자 + +- 기본적으로 난수 생성기(random number generator)는 시스템 클럭(system clock)을 사용하므로 테스트마다 다른 결과를 반환(비결정성)하게 된다. + +- 난수생성기가 매번 같은 값을 결정적으로 생성하도록 상수를 시드값으로 지정하자. + +### 단위 테스트에서 원격 시스템을 호출해서는 안 된다 + +- (느린) 원격 호출을 제거하면 단위 테스트의 속도와 이식성(portability)를 유지할 수 있다. + +### 클럭(clock)을 주입하자 + +- 특정한 간격의 시간에 의존하는 코드는 제대로 처리하지 않으면 비결정성을 유발한다. + +### 슬립(sleep)과 타임아웃(timeout)을 삼가자 + +- 다른 스레드의 실행이 어느 시점이 지나면 완료될 것이라고 가정에 의존해서는 안 된다. + +- 동시적(concurrent) 또는 비동기적(asynchronous) 코드의 테스트가 항상 일관적이지는 않다. + +### 네트워크 소켓과 파일 핸들을 닫자 + +- 테스트 실행 프레임워크는 하나의 프로세스에서 여러 테스트를 실행하므로 리소스(네트워크 소켓, 파일 핸들 등)에 누수가 발생하는 경우 그 즉시 정리되지 않는다. + +- 리소스 누수가 발생하면 비결정성을 일으킨다. + +- 또한 운영체제에서 열 수 있는 소켓과 파일 핸들 수는 정해져 있어 더 이상 새로운 리소스를 할당하지 못해 테스트가 실패할 수 있다. + +- `try-with-resource`나 `with` 블록같이 리소스의 사용 범위를 좁힐 수 있는 표준 리소스 관리 기법을 사용하자. + +### 파일과 데이터베이스에 대해 고유한 경로를 생성하자 + +- 동일한 파일 경로와 데이터베이스 위치를 사용하면 각 테스트가 서로 간섭하게 된다. + +- 정적으로 선언한 위치에 데이터를 기록하면 테스트 순서가 바뀌거나 병렬로 실행될 경우 에러를 발생시킬 수 있다. + ```java + // Before + public class UserServiceTest { + private static List testUsers = new ArrayList<>(); + + @Test + public void testAddUser() { + UserService service = new UserService(); + User newUser = new User("John"); + service.addUser(newUser); + testUsers.add(newUser); + assertEquals(1, testUsers.size()); + } + + @Test + public void testRemoveUser() { + UserService service = new UserService(); + User user = testUsers.get(0); // 무조건 testAddUser가 먼저 실행되어야 함 + service.removeUser(user); + testUsers.remove(user); + assertEquals(0, testUsers.size()); + } + } + + // After + public class UserServiceTest { + private UserService service; + private List testUsers; // 인스턴스 변수로 변경하여 각 테스트 메서드마다 독립적인 리스트 사용 + + @Before + public void setUp() { + service = new UserService(); + testUsers = new ArrayList<>(); + } + + @Test + public void testAddUser() { + User newUser = new User("John"); + service.addUser(newUser); + testUsers.add(newUser); + assertEquals(1, testUsers.size()); + } + + @Test + public void testRemoveUser() { + User user = new User("Jane"); + testUsers.add(user); + service.addUser(user); + + service.removeUser(user); + testUsers.remove(user); + assertEquals(0, testUsers.size()); + } + + @After + public void tearDown() { + testUsers.clear(); + } + } + ``` + +- 파일명, 디렉토리 경로, 데이터베이스나 테이블 이름은 동적으로 생성하자. + +### 이전 테스트의 상태를 격리하고 해제하자 + +- 하나의 테스트가 다른 테스트의 성능과 안정성에 영향을 미칠 수 있다. + +- 테스트의 성공 여부를 떠나 상태는 반드시 리셋해야 한다. + +### 테스트의 실행 순서에 의존하지 말자 + +- 순서에 대한 의존성을 가지는 패턴은 다음 문제를 갖고 있다. + + - 첫 번째 테스트가 실패하면 두 번째 테스트 역시 실패한다. + + - 두 번째 테스트는 첫 번째 테스트가 완료되기 전까지 실행할 수 없으므로 테스트의 병렬 실행이 어렵다. + + - 첫 번째 테스트를 수정하면 의도치 않게 두 번째 테스트가 실패할 수 있다. + + - 테스트 실행기의 변경으로 테스트가 다른 순서로 실행될 수 있다. + +- 테스트 사이에 로직을 공유하려면 셋업과 해제 메서드를 사용할 수 있다. + +## 개발자의 필수 체크리스트 + +### 이것만은 지키자 + +- 버그가 재현되는지 테스트를 이용해 확인해보자 + +- 단위 테스트는 모의 객체 도구를 이용해 작성하자 + +- 코드 품질 도구를 이용해 커버리지, 스타일, 복잡도를 검증하자 + +- 테스트가 사용하는 난수생성기에는 적절한 시드값을 적용하자 + +- 테스트에서 사용한 네트워크 소켓과 파일은 반드시 닫아주자 + +- 테스트에 고유한 파일 경로롸 데이터베이스 ID를 생성하자 + +- 테스트 실행 사이에 남겨진 상태는 정리하자 + +### 이것만은 피하자 + +- 새로운 테스트 도구를 추가하는 데 드는 비용을 무시하지 말자 + +- 다른 사람이 여러분을 대신해 테스트를 작성해주길 기대해서는 안 된다 + +- 코드 커버리지만으로 품질을 측정하지 말자 + +- 테스트에서 불필요한 sleep() 메서드와 타임아웃을 사용해서는 안 된다 + +- 단위 테스트에서 원격 시스템을 호출하지 말자 + +- 테스트 실행 순서에 의존하지 말자 + +## 레벨업을 위한 읽을거리 + +- 특정 테스트 기법을 선택해 그에 맞는 책을 선택하는 것을 권한다. + +- 단위 테스트(Vladimir Khorikov 집필, 임준혁 옮김): 테스트에 대한 권장 기법에 대해 좀 더 알 수 있다. + +- 테스트 주도 개발(Kent Beck 집필, 김창준 옮김): TDD에 대해 상세하게 다룬다. + +- 실용주의 프로그래머(Andrew Hunt, David Thomas 집필, 정지용 옮김): 속성 기반 테스팅(property based testing)에 대해 설명한다. + +- 탐험적 테스팅(Elisabeth Hendrickson 집필, 오광신 옮김): 탐험적 테스트를 통해 코드에 대해 학습하는 기법을 설명한다. \ No newline at end of file