* 단위 테스트를 작성해야 하는 상황
> 구현한 기능이 정상적으로 동작하는지 확인하고 싶을 때
> 변경사항을 문서화하여 다른 사람들이 이해하도록 하고 싶을 때
> 코드 수정사항이 기존 동작을 깨드리지 않았는지 확인하고 싶을 때
> 시스템의 현재 동작을 이해하고 싶을 때
> 3rd Party 코드가 더 이상 기대한 대로 동작하지 않을 경우를 알고 싶을 때
* Triple A (AAA; Arrange - Act - Assert)
> 테스트의 3단계
> Arrange (준비)
- 테스트 코드 실행 전에 시스템이 적절한 상태에 있는지 확인하고 준비
> Act (실행)
- 테스트 코드를 실행 (보통 단일 메서드를 호출)
> Assert (단언)
- 실행한 코드가 기대한대로 동작하는지 확인
(반환값, 객체들의 상태, 타겟와 다른 객체 사이의 의사소통 등)
* JUnit 의 테스트의 독립성
> JUnit 은 테스트(@Test)마다 새로운 인스턴스를 생성
> Test Order 를 따로 부여하지 않은 경우, 순서도 보장되지 않음
> 특정 테스트 코드가 다른 테스트에 영향을 주는 것을 최소화
(즉, 테스트 클래스에는 static field 를 피해야 함. 그렇지 않은 경우, 테스트가 실패하면 그 원인을 찾기 위한 엄청난 노력이 들어감)
* Matcher
> 테스트 가독성을 크게 높여줌
(마치 일반 문장처럼 왼쪽에서 오른쪽으로 읽을 수 있음)
* Hamcrest Matcher
> 기본 matcher 이외에 가장 널리 사용되는 매처
> 다음과 같은 기능을 제공
- 객체 타입을 검사
- 두 객체의 참조가 같은 인스턴스인지 검사
- 다수의 매처를 결합하여 둘 다 혹은 둘 중에 어떤 것이든 성공하는 검사
- 어떤 컬렉션이 요소를 포함하거나 조건에 부합하는지 검사
- 어떤 컬렉션이 아이템 몇 개를 모두 포함하는지 검사
- 어떤 컬렉션에 있는 모든 요소가 매처를 준수하는지 검사
> CoreMatchers
- startsWith("xxx") 등 다양한 matcher 메소드를 제공
* 부동소수점 비교
> 자바에서 부동 소수점 타입 (float 과 double) 의 수들은 근사치로 비교해야 됨
> assertThat(2.32 * 3, closeTo (6.96, 0.0005));
* 예외 테스트
> try-catch() 를 이용하고, 예외가 발생하지 않은 경우 try 문 안에 fail() 을 넣어 테스트를 실패하게 만들 수 있으나 좋은 방법은 아니다
> @Rule
public ExpectedExeption thrown = ExpectedExeption.none(); // 기대하는 예외를 정의
@Test
public void exceptionRule() {
thrown.expect(InsufficientFundsException.class); // 예외의 기대사항을 적용
thrown.expectMessage("balance only 0");
account.withdraw(100);
}
* 동작 테스트 vs 메서드 테스트
> 테스트를 작성할 때는 클래스의 종합적인 동작에 집중해야 하며 개별 메서드를 테스트한다고 생각하면 안된다
* SUT (System Under Test)
> 테스트의 Target이 되는 Production Code 를 뜻함
* Test Class 와 Target Class 간의 관계
> Test Class 는 Target Class 를 의존성을 갖지만, 그 반대는 해당되지 않는다
> 하지만, Test 작성 행위가 Production code 에 영향을 미치지 않는다는 것은 아니다. 많은 테스트를 작성할수록 설계를 변경했을 때 테스트 작성이
훨씬 용이해지는 경우가 늘어난다
> Test-friendly 한 설계를 채택할수록 테스트 작성이 편해지고 설계 자체도 더 좋아진다
* Test Code 와 Production Code 의 분리
> 같은 디렉토리 및 패키지에 넣기
- 구현하기 쉽지만 배포할 때 테스트 코드를 걷어내는 번거로움이 생긴다
- 같은 디렉토리의 파일 개수도 늘어나서 불편하다
> 별도의 디렉토리 분리하지만 같은 패키지에 넣기
- 대상 코드와 동일 패키지에 존재하기 때문에 pkg 수준의 접근 권한을 가짐
* Class 내부 데이터 및 동작에 대한 테스트
> 비공개 코드를 호출하는 테스트는 그 자체로 구현 세부사항과 결속하게 된다. 세부 사항의 변경에 따라 public 동작에 대한 Test 가 Fail 될 수 있고,
이는 테스트 품질 저하로 이어질 수 있다. 이 작은 변화에 따라 많은 테스트가 Fail 될 수 있고, 이를 수정하기 위한 노력은 테스트에 대한 시간
투자를 거부하게 만든다
> 즉, 테스트를 위한 내부 데이터를 노출은 테스트와 대상 코드 사이에 과도한 결합을 초래한다
> 내부 행위를 테스트하려는 충동이 든다면 설계에 문제가 있는 것이며, 대부분 SRP 원칙을 어겼을 확률이 높다
(테스트를 하려고 충동이 드는 private method 를 다른 클래스로 이동시켜야 한다)
* 다수의 Test Case 로 분리 및 Naming 하기
> TC 가 Fail 되었을 때, 실패한 테스트 이름이 표시되기 때문에 어느 동작에 문제가 있는지 파악하기 쉽다
> 실패한 테스트를 분석하는데 시간을 줄일 수 있다
> 모든 케이스가 실행되었음을 보장할 수 있다
(여러 개의 Test 를 하나의 Test Case 에 작성하면, 하나라도 실패하는 경우 테스트가 중단되기 때문)
* 문서로서의 테스트
> 단위 테스트는 우리가 만드는 클래스에 대한 지속적이고 믿을 수 있는문서의 역할을 해야 한다
> 자체로써 쉽게 설명할 수 없는 가능성들을 알려준다
* 의미있는 테스트 Naming 하기
> 아래와 같이 쉽게 읽고 이해할 수 있도록 만들어야 한다
- 어떤 동작을 하면 어떤 결과가 나온다
- 어떤 결과는 어떤 조건에서 발생한다
- 주어진 조건에서 어떤 일을 하면 어떤 결과가 나온다
- 어떤 일을 하면 어떤 결과가 나온다
- BDD의 Given-When-Then 양식 사용
* 테스트를 의미 있게 만들기
> 지역 변수 이름 개선하기
> 의미 있는 상수 도입하기
> Hamcrest Assertion 사용하기
> 커다란 테스트를 작게 나누어 집중적인 테스트 만들기
> 테스트 군더더기들을 도우미 메서드와 @Before 메서드로 이동하기
* @Before Method
> setup() 메소드 라고도 함
> 다수의 @Before 메서드가 있을 때, JUnit 은 어떤 실행 순서를 보장하지 않는다
* @After Method
> 각 Test 를 한 후에 실행되어, 테스트가 실패하더라도 실행됨
> 테스트로 발생하는 부산물을 정리하는 역할을 한다
* 좋지 못한 테스트는 오히려 문제를 야기시킨다
> 테스트를 사용하는 사람에게 어떤 정보도 주지 못하는 테스트
> 산발적으로 실패하는 테스트
> 어떤 가치도 증명하지 못하는 테스트
> 실행하는 데 오래 걸리는 테스트
> 코드를 충분히 커버하지 못하는 테스트
> 구현과 강하게 결합되어 있는 테스트. (작은 변화에도 다수의 테스트가 깨진다)
> 수많은 설정 고리로 점프하는 난해한 테스트
* SOLID 클래스의 설계 원칙
> 객체 지향 클래스 설계에 관한 다섯 가지 원칙
(1990년대 중반 로버트 마틴(Robert C. Martin)에 의해 소개되었고, 2000년 초 마이클 패더스(Michael Feathers) 에 의해 약어가 붙여졌다)
> SRP (Single Responsibility Princicple)
> OCP (Open Closed Principle)
> LSP (Liskov Substitution Principle)
> ISP (Interface Segregation Principle)
> DIP (Dependency Inversion Principle)
* 단위 테스트의 유지 보수 비용
> 리팩토링은 기능을 변경하지 않고 코드 구현을 바꾸는 활동이지만, 현실에서는 인터페이스가 변경되는 일이 생기고 그에 따라 테스트도 깨진다.
그에 따라 깨진 테스트를 고치는 비용이 발생한다. 하지만, 보통 돌아오는 가치가 훨씬 크기 때문에 이를 받아들인다
- 결함이 거의 없는 코드를 가질 수 있다
- 다른 코드가 깨질 것을 걱정하지 않으면서 코드를 변경할 수 있다
- 코드가 정확히 어떻게 동작하는지 쉽게 알 수 있다
> 시스템 설계 및 코드 품질이 낮아질수록 단위 테스트의 유지 보수 비용은 증가한다
* 테스트가 깨지지 않도록 보호하는 방법
> 코드 중복을 제거한다. 중복은 가장 큰 설계 문제이다
- 테스트를 따르기가 어려워진다 (라인 증가로 가독성이 떨어진다)
- 작은 코드 조각들을 단일 메서드로 추출하면 그 코드 조각들을 변경하기가 훨씬 쉽다
> 단위 테스트를 설정하는 데 많은 코드가 필요하다면 그것은 시스템 설계에 문제가 있는 것이다
- 클래스를 분할하여야 한다
> private method 를 테스트하려는 충동은 클래스가 필요 이상으로 커졌다는 의미이다
- 새 클래스나 다른 클래스로 적절하게 옮겨야 한다
> 테스트 설계가 어려운 경우 설계를 개선하여 쉽게 테스트 작성이 가능하도록 만들어야 한다
- 테스트 유지 비용을 없앨 수는 없으나 줄일 수 있다
* Stub (스텁)
> 테스트 용도로 하드 코딩한 값을 반환하는 구현체
* Dependency Injection (의존성 주입)
> 단순히 스텁을 인스턴스로 전달하거나 주입하는 것을 의미
> Mockito 활용하기 (Mock 주입하기)
* Mock은 단위 테스트 커버리지의 구멍을 만든다. 따라서, 통합 테스트를 작성하여 구멍을 막아야 한다
* 테스트 리팩토링
> Test Smell : 불필요한 테스트 코드
- try/catch 문을 테스트 코드에 작성할 필요가 없다
(exception 발생 시, Junit 에서 오류로 간주하여 처리한다)
- 특정 객체에 대하여 not-null 을 assert 후에, 추가적인 assert 를 수행할 필요가 없다.
2번째 assert 문에서 해당 객체가 null 인 경우 fail 을 발생시키기 때문에 굳이 not-null 을 체크하지 않아도 된다
> Test Smell : 추상화 누락
- 특정 객체의 각각의 member 변수들에 대한 assert 문을 작성하고 있는 경우에는 해당 객체 타입을 비교하는 matcher 를 이용하여 서로 비교한다
- Collection.size() > 0 를 assert 하는 대신에 Collection.isEmpty() 로 변경하여 가독성을 높인다
> Test Smell : 부적절한 정보
- Magic Literal 을 사용하지 않는다
이를 사용할 경우 무슨 의미로 사용되었는지 이해하기 어려우며, 직접 찾아서 분석하는 cost 가 발생한다. 따라서, 상수로 대체한다 (static final)
(Magic Literal : 상수로 선언하지 않은 숫자를 의미하며, 되도록 사용하지 않아야 한다)
> Test Smell : 부푼 생성
- 생성 로직이 복잡한 경우에는 새로운 method 로 extract 하여 단순화 시킨다
> Test Smell : 다수의 단언
- 테스트마다 여러 개의 단언이 있다는 것은 테스트 케이스를 2개 포함하고 있다는 증거이다.
하나의 테스트 케이스에는 하나의 단언만 포함하고 나머지는 새로운 테스트 케이스로 추가하면 test method 이름도 깔끔하게 만들기 쉽다
> Test Smell : 테스트와 무관한 세부 사항들
- 해당 테스트 케이스와 관련이 없는 공통된 로직들은 @Before, @After 혹은 Test Class 의 선언부로 옮겨야 한다
> Test Smell : 잘못된 구성
- AAA의 각 단계 사이에 빈줄을 넣어 읽기 쉽도록 구분시켜준다
> Test Smell : 암시적 의미
- Input 에 따른 Expected Output 을 명확히 이해할 수 있도록 Data 를 선정한다
* TDD 의 이점
> 코드가 예상한대로 동작한다는 자신감을 얻을 수 있다
> 다른 코드의 동작을 망가뜨릴 것이라는 두려움을 없앨 수 있다
* TDD 의 단계
> 실패하는 테스트 코드 작성하기
> 테스트 통과 시키기
> 이전 두 단계에서 추가되거나 변경된 코드 개선하기
* 문서로서의 테스트
> Test case 이름들은 해당 클래스의 전체 동작을 의미한다
* 멀티스레드 코드 테스트
> 동시성 처리가 필요한 코드의 테스트는 단위 테스트의 영역이 아니다. 통합 테스트로분류하는 것이 더 낫다
> 스레드 없이 다량의 코드를 테스트 할 수 있도록 설계를 변경하여, 테스트 가능한 작은 조각들에 대하여 테스트한다
> 모두 테스트 할 수 없더라도, 동시성 관련 라이브러리들이 작 동작할 것이라는 믿는다
* 데이터베이스 테스트
> Controller 를 통해서 DB 와 연결되는 경우, Controller 가 아닌 직접 DB 와 데이터를 주고받도록 테스트를 만들 수 있다
> 혹은, Controller 에 대해서 Mock 을 이용하여 테스트를 수행한다
> In-memory DB 의 경우, 실제 DB 와 동작이 다르기 때문에 완벽히 대체되어 테스트 될 수는 없다
> 테스트 데이터가 실제 데이터와 섞이는 경우, 테스트가 제대로 성공 or 실패 했는지 알기가 어렵다.
따라서, 테스트를 위한 DB를 이용하거나 실제 DB 를 이용하더라도 @Before, @After 를 이용하여 초기 상태를 유지하도록 만들어줘야 한다
* 까다로운 테스트
> 스레드와 데이터베이스와 같은 까다로운 항목들에 대해서 테스트 하는 방법
- 문제를 일으키는 다른 의존성과 분리하여 고립된 테스트를 작성한다
> 느리거나 휘발적인 코드를 Mock 으로 대체하여 의존성을 끊는다
> 필요한 경우에는 통합 테스트를 작성하되, 단순하고 집중적으로 만든다
* 프로젝트에 테스트 도입하기
> 크런치 모드에 테스트를 무시하게 되면 짧은 시간에 많은 코드를 만들겠지만 코드는 엉망이 될 확률이 높다. 제대로 동작하는지 아는 데 훨씬 많은
시간이 걸리고 엉망이 된 코드에서 결함을 고치는 기간도 오래 걸릴 것이다. 그리고 더 많은 결함이 발생할 것이다
(크런치모드 ; 공격적인 목표나 마감에 맞추려고 철야를 불사하는 기간)
> 테스트 없이 코드를 만드는 것은 매우 단기간에만 생산적이다(며칠정도). 급조한 코드를 변경하는데 더 많은 시간이 들고 이해하기도 어려워진다
> 습관의 일부가 될 수 있도록 프로젝트 초기부터 도입하는 것이 낫다
> 모든 사람이 동의하지는 않겠지만, 합의점을 늘려가야 한다
* 단위 테스트 표준 만들기
> 코드를 체크인하기 전에 어떤 테스트를 실행해야 할지 여부
> 테스트 클래스와 메서드의 이름 짓는 방식
> Hamcrest 혹은 legacy assert 사용 여부
> AAA 사용 여부
> 선호하는 Mock 툴 선택
> 체크인 테스트를 실행할 때 콘솔에 출력을 허용할지 여부
> 단위 테스트 스위트에서 느린 테스트를 분명하게 식별하고 막을 방법
* 제프의 코드 커버리지 이론
> 낮은 커버리지의 영역에서 나쁜 코드의 양도 증가한다
* 100% 커버리지는 진짜 좋은가?
> TDD 를 수행하는 개발자들은 일반적으로 정의상 90% 를 초과 달성한다
> 코드가 어디에서 커버리지가 부족한지, 어느 부분에서 커버리지가 낮아지고 있는지 확인할때만 사용해야한다
'SW 공학 > SE 서적' 카테고리의 다른 글
///실용주의 프로그래머 (0) | 2020.10.16 |
---|---|
★클린 아키텍처 / 로버트 C.마틴 / 인사이트 (0) | 2020.10.02 |
/////////★ 테스트 주도 개발 / 켄트벡 / 인사이트 (0) | 2019.12.15 |
켄트 벡의 구현 패턴 / 켄트벡 / 에이콘출판사 (0) | 2019.09.03 |
★ 소프트웨어 장인 / 산드로 마쿠소 / 길벗 (0) | 2019.08.04 |