Загружаем каталог…
Загружаем каталог…
기능에서의 상황 기능은 주어진 상황에 따라 다르게 동작한다. 예를 들어 다음 기능을 보자. 파일에서 숫자를 읽어와 숫자의 합을 구한다. 한 줄마다 한 개의 숫자를 포함한다. 이 기능을 MathUtils.sum() 메서드로 구현한다고 가정하자. 다음처럼 sum() 메서드에 파일을 인자로 전달할 것이다. sum() 메서드는 인자로 전달받은 파일에서 한 줄씩 읽어와 숫자로 변환한 뒤에 합한 값을 결과로 제공하면 된다. File datafile = new File("data.txt"); long sum = MathUtils.sum(dataFile); 하지만 이 기능을 구현하려면 고려할 것이 있다. 먼저 파일이 없는 상황을 처리해야 한다. 데이터를 읽을 파일이 없다면 인자가 잘못되었다는 익셉션을 발생하거나 문제 상황을 알려줄 수 있는 값을 리턴해야 한다. 비슷하게 데이터 중에 숫자가 아닌 잘못된 데이터가 존재하는 경우에도 알맞은 결과를 생성해야 한다. 이처럼 주어진 상황에 따라 기능 실행 결과는 달라진다. 이는 테스트 코드 구조에도 영향을 준다. 테스트 코드의 구성 요소: 상황, 실행, 결과 확인 기능은 상황에 따라 결과가 달라진다. 테스트 코드는 기능을 실행하고 그 결과를 확인하므로 상황, 실행, 결과 확인의 세 가지 요소로 테스트를 구성할 수 있다. 어떤 상황이 주어지고, 그 상황에서 기능을 실행하고, 실행한 결과를 확인하는 세 가지가 테스트 코드의 기본 골격을 이루게 된다. 상황, 실행, 결과 확인은 영어 표현 given, when, then에 대응한다. JUnit에서 상황을 설정하는 방법은 테스트할 대상에 따라 달라진다. 각 테스트 메서드마다 객체를 생성해서 상황을 설정하거나 @BeforeEach를 적용한 메서드에서 상황을 설정할 수 있다. 상황이 없는 경우도 존재할 수 있다. 실행 결과를 확인하는 쉬운 방법은 리턴 값을 사용하는 것이지만, 실행 결과가 항상 리턴 값으로 존재하는 것은 아니다. 실행 결과로 익셉션을 발생하는 것이 정상인 경우도 있다. 상황-실행-결과 확인 구조에 너무 집착하지는 말자. 이 구조가 테스트 코드를 작성하는데 도움이 되는 것은 맞지만 꼭 모든 테스트 메서드를 이 구조로 만들어야 하는 것은 아니다. 테스트 코드를 보고 테스트 내용을 이해할 수 있으면 된다. 외부 상황과 외부 결과 상황 설정이 테스트 대상으로 국한된 것은 아니다. 상황에는 외부 요인도 있다. File datafile = new File("data.txt"); long sum = MathUtils.sum(dataFile); MathUtils.sum() 메서드를 테스트하려면 파일이 존재하지 않는 상황에서의 결과도 확인해야 한다. 그렇다면 파일이 존재하지 않는 상황을 어떻게 만들 수 있을까? 가장 쉬운 방법은 존재하지 않는 파일의 경로를 사용하는 것이다. @Test void noDataFile_Then_Exception() { File dataFile = new File("badpath.txt"); assertThrows(IllegalArgumentException.class, () -> MathUtils.sum(dataFile) ); } 이 방법이 쉽긴 하지만 항상 테스트에 성공할 것이라는 보장은 없다. 우연이라도 해당 파일이 존재할 수 있기 때문이다. 테스트는 실행할 때마다 동일한 결과를 보장해야 하는데 우연에 의해 테스트 결과가 달라지면 동일한 결과를 보장할 수 없다. 이는 테스트를 신뢰할 수 없게 만들어 테스트 결과를 무시하게 만드는 요인이 될 수 있다. 더욱 확실한 방법은 명시적으로 파일이 없는 상황을 만드는 것이다. 다음은 명시적으로 파일이 없는 상황을 만드는 예를 보여준다. @Test void noDataFile_Then_Exception() { givenNoFile("badpath.txt"); File dataFile = new File("badpath.txt"); assertThrows(IllegalArgumentException.class, () -> MathUtils.sum(dataFile) ); } private void givenNoFile(String path) { File file = new File(path); if (file.exists()) { boolean deleted = file.delete(); if (!deleted) throw new RuntimeException("fail givenNoFile: " + path); } } 이 테스트에서 givenNoFile() 메서드는 해당 경로에 파일이 존재하는지 검사해서 존재할 경우 해당 파일을 삭제한다. 이렇게 함으로써 테스트가 항상 올바른 상황에서 동작한다는 것을 보장할 수 있다. 다음으로 파일이 존재하는 상황은 어떻게 만들 수 있을까? 쉬운 방법은 상황에 알맞은 파일을 미리 만들어 두는 것이다. 파일을 미리 만들지 않고 테스트 코드에서 상황에 맞는 파일을 생성하는 방법도 있다. 이 방법의 장점은 테스트 코드 안에 필요한 것이 다 있다는 것이다. 테스트 코드에서 상황을 명시적으로 구성하기 때문에 테스트 내용을 이해하기 위해 많은 파일을 볼 필요가 없다. 테스트 대상이 아닌 외부에서 결과를 확인해야 할 때도 있다. 예를 들어 처리 결과를 지정한 경로의 파일에 저장하는 기능을 생각해보자. 이 기능을 실행한 결과를 검증하려면 해당 경로에 파일이 원하는 내용으로 만들어졌는지 확인해야 한다. 외부 상태가 테스트 결과에 영향을 주지 않게 하기 테스트 코드는 한 번만 실행하고 끝나지 않는다. TDD를 진행하는 동안에도 계속 실행하고 개발이 끝난 이후에도 반복적으로 테스트를 실행해서 문제가 없는지 검증한다. 그렇기 때문에 테스트는 언제 실행해도 항상 정상적으로 동작하는 것이 중요하다. 간헐적으로 실패하거나 다른 테스트 다음에 실행해야 성공하면 테스트 결과를 믿을 수 없게 된다. 이렇게 되면 테스트가 실패해도 무감각해지고 더 나아가 테스트를 만들지 않게 된다. 회원 가입 기능을 예로 들어보자. 회원 가입 기능 테스트에는 다음을 포함한다. 중복된 ID가 이미 존재하면 가입 실패 모든 조건을 충족하면 가입 성공 두 테스트를 다음과 같이 작성했다고 가정하자. @Test void dupIdTest() { RegistReq req = new RegistReq("bkchoidup", "최범균중복"); assertThrows(DuplicateIdException.class, () -> registerService.register(req)); } @Test void registerSuccessfully() { RegistReq req = new RegistReq("bkchoi", "최범균"); registerService.register(req); Member mem = memberRepo.findById("bkchoi"); assertEquals("최범균", mem.getName()); } dupIdTest() 테스트를 검증하려면 DB의 회원 테이블에 아이디가 “bkchoidup"인 데이터를 미리 추가해야 한다. 그래야 아이디 중복 여부를 확인하는 dupIdTest()가 올바르게 동작한다. 아이디가 “bkchoi"인 데이터가 없는 상태에서 registerSuccessfully() 테스트를 실행했더니 통과했다고 하자. 이 테스트에 성공하면 DB 회원 테이블에 아이디가 "bkchoi"인 데이터가 생성된다. 이 상태에서 다시 registerSuccessfully() 테스트를 실행하면 이미 "bkchoi"인 아이디가 존재하므로 아이디 중복으로 테스트에 실패한다. 즉, DB 데이터의 상태에 따라 테스트가 성공하기도 하고 실패하기도 하는 것이다. 이렇게 외부 상태에 따라 테스트의 성공 여부가 바뀌지 않으려면 테스트 실행 전에 외부를 원하는 상태로 만들거나 테스트 실행 후에 외부 상태를 원래대로 되돌려 놓아야 한다. 예를 들어 registerSuccessfully() 테스트를 실행할 때 아이디가 "bkchoi”인 회원 데이터를 삭제해서 중복이 발생하지 않도록 만들거나 registerSuccessfully() 메서드 실행 후에 트랜잭션을 롤백하는 방법이 있다. 외부 상태와 테스트 어려움 상황과 결과에 영향을 주는 외부 요인은 파일, DBMS, 외부 서버 등 다양하다. 이들 외부 환경을 테스트에 맞게 구성하는 것이 항상 가능한 것은 아니다. 자동이체 등록 기능을 생각해보자. 이 기능은 입력받은 계좌 번호가 올바른지 확인해야 한다. 이를 위해 금융 회사에서 제공하는 REST API를 사용한다면 자동이체 등록 기능에 대한 테스트는 다음 상황에서의 결과를 확인할 수 있어야 한다. REST API 응답 결과가 유효한 계좌 번호인 상황 REST API 응답 결과가 유효하지 않은 계좌 번호인 상황 REST API 서버에 연결할 수 없는 상황 REST API 서버에서 응답을 5초 이내에 받지 못하는 상황 각 상황을 어떻게 테스트할 수 있을까? 유효한 계좌 번호와 유효하지 않은 계좌 번호는 API 제공 업체에서 정보를 받아 테스트해 볼 수 있다. 그런데 REST API 서버에 연결할 수 없는 상황이나 REST API 서버에서 지정한 시간 안에 응답을 주지 않는 상황은 어떻게 만들어낼 수 있을까? REST API 제공 업체에 잠깐만 서버를 죽여 달라고 할 수 없다. 또는 REST API 제공 업체에 응답을 일부러 7초 뒤에 주도록 수정해 달라고 부탁할 수도 없다. 실행 결과가 외부 시스템에 기록되는 경우도 있다. 필자가 경험한 프로젝트 중에는 외부 택배사에 배송 정보를 전달하기 위해 택배사가 제공한 DB 테이블을 사용한 사례가 있다. 주문이 들어오면 택배사가 제공한 DB 테이블에 필요한 데이터를 추가하는 방식으로 배송 정보를 전달했다. 동일한 테스트를 여러 번 수행할 수 있으려면 택배사가 제공한 DB 테이블에서 데이터를 삭제할 수 있어야 했는데 택배사는 해당 테이블에 대해 INSERT와 SELECT 권한만 주고 DELETE 권한은 주지 않았다. 이처럼 테스트 대상이 아닌 외부 요인은 테스트 코드에서 다루기 힘든 존재이다. 외부 상황은 테스트 코드에서 마음대로 제어할 수 없는 경우가 있다. 또한, 테스트 코드에서 생성한 외부 결과를 마음대로 초기화하기 힘들 때도 있다. 이렇게 테스트 대상의 상황과 결과에 외부 요인이 관여할 경우 대역을 사용하면 테스트 작성이 쉬워진다. 대역은 테스트 대상이 의존하는 대상의 실제 구현을 대신하는 구현인데 이 대역을 통해서 외부 상황이나 결과를 대체할 수 있다.
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[테스트 주도 개발 시작하기] 6장. 기능에서의 상황 기능은 주어진 상황에 따라 다르게 동작한다. 예를 들어 다음 기능을 보자. 파일에서 숫자를 읽어와 숫자의 합을 구한다. 한 줄마다 한 개의 숫자를 포함한다. 이 기능을 MathUtils.sum() 메서드로 구현한다고 가정하자. 다음처럼 sum() 메서드에 파일을 인자로 전달할 것이다. sum() 메서드는 인자로 전달받은 파일에서 한 줄씩 읽어와 숫자로 변환한 뒤에 합한 값을 결과로 제공하면 된다. File datafile = new File("data.txt"); long sum = MathUtils.sum(dataFile); 하지만 이 기능을 구현하려면 고려할 것이 있다. 먼저 파일이 없는 상황을 처리해야 한다. 데이터를 읽을 파일이 없다면 인자가…
Открыть источник