react-testing-library Jest는 자바스크립트 테스트 프레임워크로 js를 전반적으로 테스트하기 위한 프레임워크이다. 리액트토 js이기는 하지만, JSX를 사용하고 있으므로 일반적인 js는 아니다. 또한, 리액트의 JSX는 HTML의 DOM을 다루기 때문에 단순한 js의 테스트로 정확한 오류를 잡아내기는 어렵다. 리액트뿐만 아니라 최근 프론트엔드(Frontend) 프레임워크, 라이브러리인 앵귤러와 Vue도 js에서 DOM을 직접 다루기 때문에 일반적인 js 테스트 프레임워크로는 모든 테스트를 수행하기 어렵다. @testing-library는 이런 문제를 해결하고자 만들어진 DOM 테스팅 라이브러리(DOM Testing Library)이다. @tesing-library는 사용자 중심 방식으로..
필자는 백기선님의 '스프링 기반 REST API 개발' 강의를 들으면서 실습을 진행하던 중에 문제가 발생하였고 실습과 동일하게 설정하는 방법에 대해 기록한 내용입니다. 문제 발생 원인 실제 인프런 강의는 Maven Build Tool 환경에서 진행했으나, 필자는 철저한 Gradle 파이기 때문에, Gradle로 실습을 진행하였습니다. 문제는 Spring REST Docs 문서를 빌드할 때 발생했습니다. Maven의 경우 플러그인 설정을 추가하고 Spring REST Docs 공식 레퍼런스에 따라서 설정을 진행하고 IntelliJ IDE의 Build Tool 메뉴에서 package를 더블클릭하면, 설정했던 plugin들에 의해서 test에 설정했던 snippsets 들을 생성하여 문서를 만들..
25장. 테스트 주도 개발 패턴 우선 기본적인 전략에 관한 질문에 답해야 한다. 테스트한다는 것을 무엇을 뜻하는가? 테스트를 언제 해야 하는가? 테스트할 로직을 어떻게 고를 것인가? 테스트할 데이터를 어떻게 고를 것인가? (위에 처럼 글로 정리를 안해서 그렇지 위와 같은 문제점에 혼자서 답을 얻기 힘들었기 때문에 이 책을 읽고있는게 아닌가 하다) | 테스트(명사) | 자동화된 테스트를 만들어라! 테스트하다(test)는 '평가하다'라는 뜻의 동사다. 그 어떤 소프트웨어 엔지니어도, 아무리 작은 변화라도 테스트하지 않고 릴리즈하지는 않는다. 변화를 테스트할 수 있다고 해도, 실제로 변화를 테스트하는 것은 '테스트를 갖고 있다'는 것과는 같지 않다. | 격리된 테스트 | 테스트를 실행하는 것이 어떤 식으로 영..
테스트 코드를 실행하는데 다른 테스트코드는 괜찮았는데 저 테스트 클래스만 JUnit Vintage라는 오류가 발생했다. 이 문제를 해결해보고자 구글링을 해보았다. 먼저 JUnit5를 잘못 사용한 것이 아닌가? 하는 의심이 들었다.(실은 크게 잘못 사용할만한 부분이 없었다) 테스트코드에 대한 기초지식도 없었을 때, Junit5는 JUnit Vintage + JUnit Jupiter이다라고 하면서 공부했던 적이있는데, 어쨋거나 jUnit5의 두 요소중 하나의 요소만 테스트에 성공했으니 junit Vintage의 요소를 제대로 못받아 온게 아닌가... 즉 의존성 설정부분에서 실수한 것이 있을 것 같다는 생각이 들었다. 아래의 링크를 참조해서 설정을 해보았다. https://flyburi.com/607 [Jun..
프로그래밍 공부/React
스무디 한 잔 마시며 끝내는 리액트 + TDD - 4장 리액트 테스트 - react-testing-library
로ᄏl2021. 7. 23. 00:25
728x90
react-testing-library
Jest는 자바스크립트 테스트 프레임워크로 js를 전반적으로 테스트하기 위한 프레임워크이다. 리액트토 js이기는 하지만, JSX를 사용하고 있으므로 일반적인 js는 아니다. 또한, 리액트의 JSX는 HTML의 DOM을 다루기 때문에 단순한 js의 테스트로 정확한 오류를 잡아내기는 어렵다.
리액트뿐만 아니라 최근 프론트엔드(Frontend) 프레임워크, 라이브러리인 앵귤러와 Vue도 js에서 DOM을 직접 다루기 때문에 일반적인 js 테스트 프레임워크로는 모든 테스트를 수행하기 어렵다.
@testing-library는 이런 문제를 해결하고자 만들어진 DOM 테스팅 라이브러리(DOM Testing Library)이다. @tesing-library는 사용자 중심 방식으로 UI 컴포넌트를 테스트하는 데 도움을 주는 라이브러리이다.
@testing-library는 리액트 이외에도 앵귤러, Vue, Sevele 등 UI 컴포넌트를 테스트하기 위한 매우 가벼운 솔루션으로, 유지보수가 가능한 리액트 컴포넌트용 테스트 코드를 확인할 수 있다
react-testing-library의 장점
react-testing-library는 리액트 컴포넌트를 테스트하기 위한 매우 가벼운 솔루션으로, 유지보수가 가능한 리액트 컴포넌트용 테스트 코드를 작성할 수 있다.
react-testing-library도 같은 원리로 리액트 컴포넌트용 테스트 코드 작성을 도와준다. 이 말은 즉, 테스트 코드를 작성할 때 컴포넌트 세부 구현 사항을 포함하지 않으면서도 신뢰할 수 있는 테스트 코드 작성에 도움을 준다. 이렇게 컴포넌트의 세부 구현 사하을 포함하지 않은 테스트 코드를 작성하면 컴포넌트의 세부 구현 부분을 리팩토링하여도 테스트 코드를 수정할 필요가 없다. 이로 인해 한번 작성한 테스트 코드는 긴 시간 유지할 수 있으며 오랜 기간 유지 가능한 테스트 코드는 테스트 코드를 자주 수정하지 않아도 되므로 개발 생산성을 향상 시켜준다.
react-testing-library는 react-dom위에서 동작한다. 다른 테스트 프레임워크는 리액트 컴포넌트를 단순히 인스턴스로 처리하지만, react-testing-library는 실제 DOM 노드에서 작동하므로 더 신뢰할 수 있는 테스트를 할 수 있다.
react-testing-library는 사용자 중심의 테스트 유틸리티를 제공한다. 사용자 중심의 테스트 유틸리티란 react-testing-library를 사용하여 DOM을 찾는 기능들이 실제 사용자 DOM을 사용하는 방식과 유사한 형태로 제공되고 있음을 의미한다. 예를 들어, 텍스트로 폼(Form)의 요소를 찾거나 텍스트에서 링크 및 버튼 등을 찾는 테스트 코드는 마치 사용자가 화면을 보면서 찾는 것과 같은 형태로 테스트 코드를 작성할 수 있도록 돕는다.
react-testing-library는 테스트 실행기 또는 프레임워크가 아니다. 따라서 react-testing-library를 사용하기 위해서는 다른 테스트 프레임워크와 함께 사용해야 한다. react-testing-library의 공식 사이트에서는 함께 사용할 테스트 프레임워크로 Jest를 추천하고 있지만, react-testing-library는 특정 테스트 프레임워크에 종속되어 있지 않았으므로 Jest이 외에 다른 테스트 프레임워크에서도 동작한다.
프로젝트 준비
react-testing-library는 리액트의 컴포넌트를 테스트하기 위한 라이브러리이므로 react-testing-library를 사용하기 위해서는 리액트 프로젝트를 준비할 필요가 있다.
다음 명령어를 실행하여 react-testing-library를 사용할 리액트 리액트 프로젝트를 생성하자.
npx create-react-app react-testing-library-test
react-testing-library 설치
react-testing-library는 Jest와 마찬가지로 create-react-app으로 생성한 리액트 프로젝트에 기본적으로 같이 설치된다. 우리는 create-react-app을 사용하여 리액트 프로젝트를 생성하였으므로 추가적인 설치 과정을 진행하지 않아도 된다.
만약 create-react-app으로 프로젝트를 생성하지 않는 경우에는 다음의 명령어를 실행하여 react-testing-library를 설치해야 한다.
npm install --save-dev @testing-library/react
사용 방법
create-react-app으로 생성한 리액트 프로젝트의 src 폴더를 열어보면 리액트 컴포넌트가 작성된 App.js 파일과 그에 관한 테스트 코드가 작성된 App.test.js 파일을 확인할 수 있다.
App.test.js 파일을 보면 @testing-library/react에서 render와 screen을 불러와 테스트에 사용하고 있음을 알 수 있다. 또한, Jest의 test 함수(it 함수와 같은 역할을 하는 함수)를 사용하여 테스트 명세를 작성한 테스트 코드임을 알 수 있다.
테스트 코드를 자세히 살펴보면 우선, @testing-library/react에서 불러온 render 함수는 리액트 컴포넌트를 화면에 표시하기 위함이고, screen은 리액트 컴포넌트가 표시된 화면을 의미한다. 여기서는 이해하기 쉽도록 표시된 화면이라고 표현했지만, 실제로는 화면에 표시되지는 않는다. 여기서 render 함수는 메모리상에 돔을 만들고 screen을 통해 해당 돔에 접근하는 것을 의미한다.
react-testing-library의 render 함수를 사용하여 App이라는 컴포넌트를 렌더링하였다. 이렇게 렌더링된 컴포넌트에서 screen.getByText를 통해 화면에서 'learn react'라는 글자를 가지고 있는 돔 요소(DOM Element)를 찾고 찾은 요소를 Jest의 expect().toBeInTheDocument()를 사용하여 돔에 표시되어 있는지 확인하였다.
이제 테스트 코드를 실행하기 위해 package.json 파일을 열어 스크립트의 내용을 확인해본다.
react-testing-library의 render 함수는 유용한 오브젝트들을 반환하는데 그중 하나인 container를 받아서 사용하였다. container는 리액트 컴포넌트에서 화면에 표시되는 부분을 담고있는 오브젝트이다. 이 container를 사용하여 getElementsByClassName 함수로 화면에 표시되고 있는 <img />를 클래스명으로 찾아가져 왔으며 가져온 HTML 요소 한개가 존재하는지를 toHaveLength 함수로 체크하였다.
또한, getElementsByClassName 함수를 통해 우리가 찾고자 하는 이미지가 존재하는지 확인하였고 해당 이미지가 실제로 우리가 원하는 이미지를 표시하고 있는지 toHaveAttribute 함수를 사용하여 <img/> 태그의 src를 가져와 비교하였다.
현재 npm run test로 Jest의 테스트가 계속 실행 중이므로 이렇게 App.test.js 파일을 수정한 후 저장하면 명령 프롬프트에 다음과 같은 결과가 표시되는 것을 확인할 수 있다.
Watch Usage: Press w to show more.
PASS src/App.test.js
✓ renders learn react link (30 ms)
<App />
✓ renders component correctly (5 ms)
이제 화면에 설명문(<p/>)이 잘 표시되는지 확인해 보자. 화면에 설명문이 잘 표시되는지 확인하기 위해 테스트 명세를 다음과 같이 수정한다.
describe('<App />', () => {
it('renders component correctly', () => {
const {container} = render(<App/>);
...
expect(container.getElementsByTagName('p')).toHaveLength(1);
expect(container.getElementsByTagName('p')[0]).toHaveTextContent(
'Edit src/App.js and save to reload.'
);
});
});
이번에는 container 오브젝트에서 getElementsByTagName 함수를 사용하여 <p> 태그를 찾고 해당 태그가 한 개 존재하고 있음을 toHaveLength 함수를 통해 테스트하였다. 또한, 해당 <p> 태그가 우리가 화면에 표시되길 원하는 문자열(Edit src/App.js and save to reload.)을 잘 표시하고 있는지 toHaveTestContent 함수를 사용하여 테스트하였다.
이제 App 컴포넌트의 모든 요소가 화면에 잘 표시되는 것을 테스트 코드를 통해 확인하였다. 마지막으로, 화면에 표시되는 내용이 변경되었는지 알기 위해 스냅샷 테스트를 추가한다.
이렇게 파일을 수정하여 스냅샷 테스트를 추가한 후 저장하면 이전과는 다르게 다음과 같은 화면을 명령 프롬프트에서 확인할 수 있다.
또한, src/__snapshots__/App.test.js.snap이라는 파일이 생성된 것을 확인할 수 있다. 파일을 열어 내용을 확인해 보면 App 컴포넌트가 화면에 렌더링될 때 표시되는 HTML 내용이 저장된 것을 확인할 수 있다.
이렇게 저장된 스냅샷은 App 컴포넌트가 수정되어 화면에 표시되는 HTML 구조가 변경되면 에러를 표시하게 된다. 이렇게 스냅샷은 화면에 표시되는 컴포넌트가 변경되었는지 감지하기 위한 테스트로 많이 사용된다.
스냅샷 테스트가 제대로 동작하는지 확인하기 위해 App.js 파일을 열어 다음과 같이 수정한다.
만약 이 상태에서 스냅샷의 내용을 변경하면, 아래와 같이 테스트 코드가 실패하는 것을 확인할 수 있다.
이런 스냅샷 테스트는 우리가 리액트 컴포넌트를 수정했을 때, 수정한 내용이 의도치 않게 화면 표시를 변경하는 실수를 알 수 있게 해준다.
만약 컴포넌트를 수정하여 화면 표시가 변경된 것이 의도된 수정이었다면 스냅샷 테스트로 저장된 파일을 업데이트해 주어야 한다. 터미널에 위의 그림과 같은 에러가 표시된 상태에서 키보드의 'u' 키를 누르면 스냅샷으로 생성된 파일이 업데이트된다. 그러면 새롭게 업데이트된 스냅샷 파일이 다시 기준이 되어 변경을 감지하고 에러를 표시하게 된다.
필자는 백기선님의 '스프링 기반 REST API 개발' 강의를 들으면서 실습을 진행하던 중에 문제가 발생하였고 실습과 동일하게 설정하는 방법에 대해 기록한 내용입니다.
문제 발생 원인
실제 인프런 강의는 Maven Build Tool 환경에서 진행했으나, 필자는 철저한 Gradle 파이기 때문에, Gradle로 실습을 진행하였습니다. 문제는 Spring REST Docs 문서를 빌드할 때 발생했습니다. Maven의 경우 플러그인 설정을 추가하고 Spring REST Docs 공식 레퍼런스에 따라서 설정을 진행하고 IntelliJ IDE의 Build Tool 메뉴에서 package를 더블클릭하면, 설정했던 plugin들에 의해서 test에 설정했던 snippsets 들을 생성하여 문서를 만들고 문서를 SpringBoot가 기본적으로 지원하는 static directory에 들어가서 우리가 앱 서버를 띄우면 API 문서 페이지를 바로 View에서 확인할 수가 있다.
문서는 target 폴더에 generated-docs 폴더 아래에 index.html(Spring REST Docs 이름을 index.html으로 설정했다고 가정) 파일이 생성되는 것을 확인할 수 있다.
그와 더불어 아래와 같이 target/static/docs 폴더 아래에도 index.html이 생성된 것을 확인할 수 있다.
이렇게 생성이 된 것을 확인했으면, 웹 서버를 띄우고 URI에 docs/index.html을 입력하면, Spring REST Docs를 조회할 수 있다!
하지만 문제점은 Gradle로 Spring REST Docs을 사용하기 위한 설정을 하게 되면(레퍼런스 동일) target 폴더에 snippsets이라던지 docs가 생성되는 Maven과는 달리 아래와 같이 build directory에 snippsets와 docs가 생성된다.
예상했을지 모르겠지만, 이 docs의 생성경로가 Maven과 달라서인지, 웹 서버를 실행하여 uri에 doc/index.html을 입력해도 Spring REST Docs를 조회할 수 없다. 이런 경우에 웹 서버를 통해서 생성한 Spring REST Docs를 조회할 수 있도록 하려면 어떻게 해야할까? 이 방법이 최선인지는 확실치 않지만, 방법이 하나 있다. 간략히 설명하면 build에 생성된 Spring REST Docs 문서를 static/doc 경로에 복사해주면 된다. 단순히 build.gradle에 아래의 코드를 추가하면 된다.
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/asciidoc/html5/")
into file("src/main/resources/static/doc")
}
build {
dependsOn copyDocument
}
그 후 build를 실행하면 아래와 같이 static/doc 폴더에 index.html 파일이 복사된 것을 알 수 있다.
(위에 처럼 글로 정리를 안해서 그렇지 위와 같은 문제점에 혼자서 답을 얻기 힘들었기 때문에 이 책을 읽고있는게 아닌가 하다)
| 테스트(명사) |
자동화된 테스트를 만들어라! 테스트하다(test)는 '평가하다'라는 뜻의 동사다. 그 어떤 소프트웨어 엔지니어도, 아무리 작은 변화라도 테스트하지 않고 릴리즈하지는 않는다. 변화를 테스트할 수 있다고 해도, 실제로 변화를 테스트하는 것은 '테스트를 갖고 있다'는 것과는 같지 않다.
| 격리된 테스트 |
테스트를 실행하는 것이 어떤 식으로 영향을 미쳐야 좋은가? 아무 영향이 없어야 한다. 각각의 테스트는 다른 테스트와 완전히 독립적이어야 한다. 즉 문제가 하나면 테스트도 하나만 실패해야 하고, 문제가 둘이면 테스트도 두 개만 실패해야 한다.
격리된 테스트가 암묵적으로 내포하는 특징 중 하나는 테스트가 실행순서에 독립적이게 된다는 점이다. (테스트의 일부만 실행해보고 싶으면, 선행 테스트가 실행되지 않아서 내가 고른 테스트들이 실패하지 않을까 걱정할 필요 없이 그렇게 할 수 있어야 한다)
성능 문제는 테스트가 데이터를 공유해야 하는 이유로 자주 언급된다. 격리된 테스트가 내포하는게 또 하나 있는데, 이는 주어진 문제를 작은 단위로 분리하기 위해 노력해서 각 테스트를 실행하기 위한 환경을 쉽고 빠르게 세팅할 수 있게 해야 한다는 것이다. 테스트를 격리하기 위한 작업은 결과적으로 시스템이 응집도는 높고, 결합도는 낮은 객체의 모음으로 구성되도록 한다.
| 테스트 목록 |
뭘 테스트해야 하나? 시작하기 전에 작성해야 할 테스트 목록을 모두 적어둘 것. 우선 구현할 필요가 있는 모든 오퍼레이션의 사용예들을 적는다. 그 다음, 아직 존재하지 않는 오퍼레이션에 대해서는 해당 오퍼레이션의 널 버전(아무 일도 하지 않는 버전)을 리스트에 적는다. 마지막으로 깔끔한 코드를 얻기 위해 이번 작업을 끝내기 전에 반드시 해야할 리팩토링 목록을 적는다. 테스트의 윤곽만 잡는 대신, 한 걸음 더 나아가 테스트를 전부 구현할 수도 있다. (But 그렇게 추천하지는 않음, 실제로 초록 막대를 보는데 걸리는 시간이 상당히 길어지기 때문에 TDD와는 조금 거리가 멀어진다고 생각이 듦)
테스트를 통과하게 만드는 과정에서 우리가 작성한 코드들은 새로운 테스트가 필요함을 암시적으로 알려줄 것이다. 이 새 테스트를 리팩토링과 마찬가지로 할일 목록에 적어 놓아라. 세션이 끝났을 때 목록에 남아 있는 항목들은 따로 신경 쓸 필요가 있다. 어떤 기능을 하나 진행하는 중이라면 다음 번에도 똑같은 목록을 사용하라. 현재 작업 범위를 넘어서는 큰 리팩토링 거리를 발견한다면, '다음' 할일 목록으로 옮겨라.
| 테스트 우선 |
테스트를 언제 작성하는 것이 좋을까? 테스트 대상이 되는 코드를 작성하기 직전에 작성하는 것이 좋다. 코드를 작성한 후에는 테스트를 만들지 않을 것이다. 프로그래머로서 우리의 목표는 기능이 실행되도록 만드는 것이다. 하지만 또 한편으로는 프로그램의 설계에 대해 생각해볼 시간도 필요하고 작업 범위를 조절할 방법도 필요할 것이다.
| 단언 우선 |
테스트를 작성할 때 단언(assert)를 언제쯤 쓸까? 단언을 제일 먼저 쓰고 시작하라.
시스템을 개발할 때 무슨 일부터 하는가? 완료된 시스템이 어떨 거라고 알려주는 이야기부터 작성한다.
특정 기능을 개발할 때 무슨 일부터 하는가? 기능이 완료되면 통과할 수 있는 테스트부터 작성한다.
테스트를 개발할 때 무슨 일부터 하는가? 완료될 때 통과해야 할 단언부터 작성한다.
단언을 먼저 작성하면 작업을 단순하게 만드는 강력한 효과를 볼 수 있다. 구현에대해 전혀 고려하지 않고 테스트만 작성할 때도 사실 우리는 몇 가지 문제들을 한번에 해결하는 것이다.
테스트하고자 하는 기능이 어디에 속하는 걸까? 기존의 메서드를 수정해야 하나, 기존의 클래스에 새로운 메서드를 추가해야 하나, 아니면 이름이 같은 메서드를 새 장소에? 또는 새 클래스에?
메서드 이름은 뭐라고 해야 하나?
올바른 결과를 어떤 식으로 검사할 것인가?
이 테스트가 제안하는 또 다른 테스트에는 뭐가 있을까?
이 문제를 한번에 잘 해결하기에는 쉽지가 않다. '올바른 결과는 무엇인가?', '어떤 식으로 검사할 것인가?;는 나머지 문제에서 쉽게 분리할 수 있다.
예를 들어, 소켓을 통해 다른 시스템과 통신하려 한다고 가정할 때, 통신을 마친 후 소켓은 닫혀 있고, 소켓에서 문자열 'abc'를 읽어와야 한다고 치자.
아직 실제 용도에 맞게 이름을 수정하는 일이 남아 있긴 하지만 지금까지 아주 작은 단계로 빠른 피드백을 받으며 테스트의 아웃라인을 만들었다.
| 테스트 데이터 |
테스트할 때 어떤 데이터를 사용해야 하는가? 테스트를 읽을 때 쉽고 따라가기 좋을 만한 데이터를 사용하라. 데이터 작성에도 청중이 존재한다. 단지 데이터 값을 산발하기 위해 데이터 값을 산발하지 마라. 데이터 간에 차이가 있다면 그 속에 어떤 의미가 있어야 한다. (1과 2사이에 어떠한 개념적 차이도 없다면 1을 사용하라)
테스트 데이터 패턴이 완전한 확신을 얻지 않아도 되는 라이선스는 아니다. 만약 시스템이 여러 입력을 다루어야 한다면 테스트 역시 여러 입력을 반영해야 한다. 하지만 세 항목만으로 동일한 설계와 구현을 이끌어낼 수 있다면 굳이 항목을 열 개나 나열할 필요는 없다.
테스트 데이터 패턴의 한 가지 트릭은 여러 의미를 담는 동일한 상수를 쓰지 않는 것이다. 만약 plus() 메서드를 구현하려고 한다면 고전적 예제인 2+2 혹은 1+1을 쓰고 싶을 것이다. 만약 구현에서 인자의 순서가 뒤집힌다면 어떻게 될까? (plus()에서야 순서가 뒤집혀도 상관이 없겠지만 다른 메서드는 아닐수도 있다 )우리가 첫 번째 인자로 2를 썼다면 두 번째 인자는 3을 써야 한다. 테스트 데이터에 대한 대안은 실제 세상에서 얻어진 실제 데이터를 사용하는 것이다. 실제 데이터는 다음과 같은 경우에 유용하다.
실제 실행을 통해 수집한 외부 이벤트의 결과를 이용하여 실시간 시스템을 테스트하고자 하는 경우.
예전 시스템의 출력과 현재 시스템의 출력을 비교하고자 하는 경우 (병렬 테스팅)
시뮬레이션 시스템을 리팩토링한 후 기존과 정확히 동일한 결과가 나오는지 확인하고자 할 경우. 특히 부동소수점 값의 정확성이 문제가 될 수 있다.
| 명백한 데이터 |
데이터의 의도를 어떻게 표현할 것인가? 테스트 자체에 예상되는 값과 실제 값을 포함하고 이 둘 사이의 관계를 드러내기 위해 노력하라. 테스트를 작성할 때는 컴퓨터뿐 아니라 후에 코드를 읽을 다른 사람들도 생각해야한다.
예를 들어서, 한 통화를 다른 통화로 환전하려고 하는데, 이 거래에는 수수료 1.5%가 붙는다. USD에서 GBP로 교환하는 환율이 2:1이라면 $100를 환전하려면 50GBP - 1.5% = 49.25GBP여야 한다. 이 테스트는 아래와 같이 쓸 수 있다.
Bank bank = new Bank();
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP);
assertEquals(new Note(49.25, "GBP"), result);
또는 계산을 더 명확히 표현할 수도 있다 .
Bank bank = new Bank();
bank.addRate("USD", "GBP", 2);
bank.commission(0.015);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / 2 * (1 - 0.015), "GBP"), result);
이 테스트에서는 입력으로 사용된 숫자와 예상되는 결과 사이의 관계를 읽어낼 수가 있다.
명백한 데이터가 주는 또 다른 이점은 프로그래밍이 더 쉬워진다는 것이다. 단언 부분에 일단 수식을 써놓으면 다음으로 무엇을 해야 할지 쉽게알게 된다. 이런 경우 어떻게든 나눗셈과 곱셈을 수행할 프로그램을 만들어야 한다는 걸 알게 되는 것이다. 이 오퍼레이션이 어디에 속할지를 점진적으로 알아내기 위해 가짜 구현을 해볼 수도 있다.
명백한 데이터는 코드에 매직넘버를 쓰지 말라는 것에 대한 예외적인 규칙일 수도 있다. 단일 메서드의 범위에서라면 어떤 매직넘버 사이의 관계는 명백하다. 하지만 이미 정의된 기호 상수가 있다면 나는 그것을 사용할 것이다.
26장. 빨간 막대 패턴
이 패턴들은 테스트를 언제 어디에 작성할 것인지, 테스트 작성을 언제 멈출지에 대한 것이다.
| 한 단계 테스트 |
목록에서 다음 테스트를 고를 때 무엇을 기준으로 할 것인가? 본인에게 새로운 무언가를 가르쳐줄 수 있으며, 구현할 수 있다는 확신이 드는 테스트를 고를 것.
각 테스트는 우리를 최종 목표로 한 단계 진전시켜 줄 수 있어야 한다. 다음 테스트 목록 중 무엇을 고르는게 좋을까?
더하기
빼기
곱하기
나누기
비슷한 것 더하기
동치성(equals)
널과의 동치성(equals null)
널 환전
한 개의 통화를 환전하기
두 개의 통화를 환전하기
환시세
이중에서 어떤 것을 두고 해도 상관없다. (본인이 진짜로 구현이 가능하다고 확신하는 것을 고르자)
전체 계산 중 간단한 하나의 사례를 나타내는 테스트에서 시작했다면, 이 테스트를 통해 자라는 프로그램은 하향식(top-down)으로 작성된 것으로 보일 수 있다. 반면 전체의 작은 한 조각을 나타내는 테스트에서 시작하여 조금씩 붙여나가는 식이었다면, 이 프로그램은 상향식(bottom-up)으로 작성된 것으로 보일 수 있다.
사실은 상향식, 하향식 둘 다 TDD의 프로세스를 효과적으로 설명해 줄 수 없다. 첫째로 이와 같은 수직적 메타포는 프로그램이 시간에 따라 어떻게 변해 가는지에 대한 단순화된 시각일 뿐이다. 이보다 성장이란 단어를 보자. '성장'은 일종의 자기유사성을 가진 피드백 고리를 암시하는데, 이 피드백 고리에서는 환경이 프로그램에 영향을 주고 프로그램이 다시 환경에 영향을 준다. 둘째로, 만약 메타포가 어떤 방향성을 가질 필요가 있다면 (상향 혹은 하향보다는) '아는 것에서 모르는 것으로' 라는 방향이 유용할 것이다. '아는 것에서 모르는 것으로'는 우리가 어느 정도의 지식과 경험을 가지고 개발을 시작한다는 점, 개발하는 중에 새로운 것을 배우게 될 것임을 예상한다는 점 등을 암시한다. (우리는 아는 것에서 모르는 것으로 성장하는 프로그램을 갖게 된다)
| 시작 테스트 |
어떤 테스트부터 시작하는 게 좋을까? 오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트할 것.
새 오퍼레이션에 대한 첫 질문은 다음과 같을 것이다. "이 오퍼레이션을 어디에 넣어야 하지?" 이 질문에 답하기 전까지는 테스트에 뭘 적어야 할지 알 수 없을 것이다. 한 번에 한 문제만 해결하자는 의미에서 다른 질문은 다 빼고 딱 이 질문만 생각할 방법은 무엇인가?
첫 걸음으로 현실적인 테스트를 하나 작성한다면 상당히 많은 문제를 한번에 해결해야 하는 상황이 될 것이다.
이 오퍼레이션을 어디에 두어야 하나?
적절한 입력 값은 무엇인가?
이 입력들이 주어졌을 때 적절한 출력은 무엇인가?
현실적인 테스트 하나로 시작하면 너무 오랫동안 피드백이 없을 것이다. 빨강/초록/리팩토링, 빨강/초록/리팩토링. 우리는 이 고리가 몇 분 이내로 반복되길 원할 것이다.
정말 발견하기 쉬운 입력과 출력을 사용하면 이 시간을 짧게 줄일 수 있다. (무슨말인지 이해가 잘 안된다. 걱정하지말자 책에서 설명하는 예제를 곧이 곧대로 적어보겠다.... 고..고갱뉨 당황하쎠서요)
예를 들어 XP 뉴스그룹에 누군가가 다각형 축소기를 테스트 우선으로 어떻게 작성할지 질문했다. 입력은 다각형 그물이고, 출력은 정확하게 똑같은 표면이면서 가능한 한 최소 개수의 다각형으로 구성된 다각형 그물이 된다. "테스트를 작동하도록 하는 데 박사 학위 논문을 읽어야 하는 경우, 이 문제를 어떻게 테스트 주도로 접근할 수 있을까요?" 시작 테스트 패턴이 이 문제에 대한 답을 준다.
출력이 입력과 같은 경우가 있다. 어떤 형상(configuration)의 다각형들은 이미 정규화되어 있고 더 축소할 수 없다.
입력은 가능한 한 적어야 한다. 이를테면 다각형 하나 또는 아예 비어있는 다각형 목록일 수도 있다.
나는 다음 시작 테스트를 작성했다.
Reducer reducer = new Reducer(new Polygon());
assertEquals(0, reducer.result().npoints);
첫 번째 테스트가 돌아간다. 이제 목록에 있는 나머지 테스트를 처리 할 차례다.
한 단계 테스트는 시작 테스트에도 적용된다. 본인에게 뭔가를 가르쳐줄 수 있으면서도 빠르게 구현할 수 있는 테스트를 선택하라. 만약 본인이 어떤 애플리케이션을 n번째 구현하고 있다면, 오퍼레이션을 한두 개 필요로 하는 테스트를 하나 골라라. 본인 스스로 그걸 작동하게 할 수 있을 거라 자신할 것이다. 많은 경우 나의 시작 테스트는 그 이후의 테스트에 비해 좀더 높은 차원의 테스트로, 애플리케이션 테스트와 비슷하다.
| 설명 테스트 |
자동화된 테스트가 더 널리 쓰이게 하려면 어떻게 해야 할까? 테스트를 통해 설명을 요청하고 테스트를 통해 설명하라. TDD의 사용을 강요하는 것 만큼 TDD가 퍼지는 것을 빨리 막는 방법은 없다. 타인의 일하는 방식을 강제로 바꿀 수는 없다. 어떻게 해야 하나? 단순한 시작법은 테스트를 이용하여 묻고, 테스트를 이용하여 설명하는 것이다.
| 학습 테스트 |
외부에서 만든 소프트웨어에 대한 테스트를 작성해야 할 때도 있을까? 패키지의 새로운 기능을 처음으로 사용해보기 전에 작성할 수 있다. 예를들어 자바의 모바일 정보 기기 프로파일(MIDP) 라이브러리를 기반으로 뭔가를 만들어야 한다고 치자. RecordStore에 어떤 데이터를 저장하고 이를 받아오고자 한다. 그냥 코딩하고선 그게 잘 돌아가길 바라는 게 좋을까? 물론 그것도 하나의 방법이기는 하다. 한 가지 대안은 우리가 이제 막 새 클래스의 새 메서드를 하나 사용한다는 것을 알아채는 것이다. 그냥 바로 사용하는 대신 API가 우리 예상대로 실행된다는 것을 확인해줄 만한 작은 테스트를 만들어 보는 것이다. 즉 다음과 같다.
public void setUp() {
store = RecordStore.openRecordStore("testing", true);
}
public void tearDown() {
RecordStore.deleteRecordStore("testing");
}
public void testStore() {
int id = store.addRecord(new byte[] {5, 6}, 0, 2);
assertEquals(2, store.getRecordSize(id);
byte[] buffer = new byte[2];
assertEquals(2, store.getRecord(id, buffer, 0));
assertEquals(5, buffer[0]);
assertEquals(6, buffer[1]);
}
만약 우리가 API를 제대로 이해했다면 이 테스트는 한번에 통과할 것이다. 아래는 학습 테스트를 작성하는 관례를 설명한다.
패키지의 새 버전이 도착하면 우선 테스트를 실행한다(그리고 필요하다면 수정한다).
만약 테스트가 통과되지 않는다면 애플리케이션 역시 실행되지 않을 것이 뻔하기 때문에 애플리 케이션을 실행해볼 필요도 없다.
일단 테스트가 통과한다면 애플리케이션은 항상 제대로 돌아갈 것이다
| 또 다른 테스트 |
어떻게 하면 주제에서 벗어나지 않고 기술적인 논의를 계속할 수 있을까? 주제와 무관한 아이디어가 떠오르면 이에 대한 테스트를 할일 목록에 적어놓고 다시 주제로 돌아와라.
대화를 엄격하게 한 주제로 묶는 것은 훌륭한 아이디어를 억압하는 최고의 방법이다. 하루 온종일 비생산적인 날들을 보낸 경험에서, 내가 가야 할 길을 놓치지 않는 것이 때로는 최선임을 배웠다. 새 아이디어가 떠오르면 존중하고 맞이하되 그것이 내 주의를 흩뜨리지 않게 한다. 그 아이디어를 리스트에 적어놓고는 하던 일로 다시 돌아간다.
| 회귀 테스트 |
시스템 장애가 보고될 때 우리는 무슨 일을 제일 먼저 하는가? 그 장애로 인하여 실패하는 테스트, 그리고 통과할 경우엔 장애가 수정되었다고 볼 수 있는 테스트를 가장 간단하게 작성하라.
회귀 테스트(regression test)란, 사실 여러분에게 완벽한 선견지명이 있다면, 처음 코딩할 때 작성했어야 하는 테스트다. 회귀 테스트를 작성할 때는 이 테스트를 작성해야 한다는 사실을 어떻게 하면 애초에 알 수 있었을지 항상 생각해보라.
전체 애플리케이션 차원에서 테스트를 수행하는 것에서도 가치를 얻을 수 있다. 애플리케이션 차원의 회귀 테스트는 시스템의 사용자들이 여러분에게 정확히 무엇을 기대했으며 무엇이 잘못되었는지 말할 기회를 준다. 좀더 작은 차원에서의 회귀 테스트는 당신의 테스트를 개선하는 방법이된다. 기괴할 정도로 큰 음수에 대한 결함보고서가 있을 수 있다. 여기에서는 테스트 목록을 작성할 때 정수 롤오버를 테스트할 필요가 있다는 것을 배울 수 있다.
시스템 장애를 손쉽게 격리시킬 수 없다면 리팩토링해야 한다. 이러한 종류의 장애가 있다는 것은, 시스템의 설계가 아직 끝나지 않았다는 뜻이다.
| 휴식 |
지치고 고난에 빠졌을 땐 뭘 해야 하나? 그럴 땐 좀 쉬는게 좋다.
당신이 결정한 것에 대한 감정적 책임과 당신이 타이핑해 넣은 문자들을 손에서 깨끗이 씻어 버리도록 하라. 종종 이 정도의 거리 두기를 통해 당신에게 부족했던 아이디어가 튀어나올 수 있다. 다음과 같은 생각이 들면 여러분은 벌떡 일어날 것이다 "매개 변수를 뒤집은 상태에서 시도한 적은 없었지!" 어찌됐건간에 좀 휴식을 취하라. 만약 그러한 아이디어를 얻지 못한다면, 현재 세션의 목적을 다시 검토해 보라. 여전히 현실적인가, 아니면 새로운 목적을 골라야 하는가? 당신이 이루려고 노력했던 것이 불가능한 건 아닌가? 만약 그렇다면 팀에는 어떤 의미가 있나?
데이브 웅가(Dave Ungar)는 이걸 샤워 방법론이라고 부른다. 키보드로 뭘 쳐야 할지 알면, 그걸 치면 된다. 뭘 해야 할지 모르겠으면 샤워하러 가서 뭘 해야 할지 생각날 때까지 계속 샤워를 한다. 그의 방법론을 따른다면 많은 팀들이 더 행복해질 것이고 생산성도 향상될 것이며 냄새도 훨씬 덜 날것이다. TDD는 웅가의 샤워 방법론을 정제한 것이다. 키보드로 뭘 쳐야 할지 알면, 명백한 구현을 한다. 잘 모르겠다면 가짜 구현을 한다. 올바른 설계가 명확하지 않다면 삼각측량 기법을 사용한다. 그래도 모르겠다면 샤워나 하러 가는 거다.
| 다시 하기 |
길을 잃은 느낌이 들 땐 어떻게 할까? 코드를 다 지워버리고 처음부터 다시 해보자.
한 시간 전까지만 해도 잘 돌던 코드가 뒤죽박죽이 됐다. 다음 테스트를 어떻게 통과시켜야 할지도 모르겠고, 앞으로 20개나 되는 테스트를 다 구현해야 한다. 이런일은 자주 있다. 내 본능적인 반응은 꼬인 코드를 계속 진행할 수 있을만큼만 풀어놓자는 것이다. 숙고해보기 위해 잠깐 멈추어 생각해보면, 처음부터 다시 하는 게 항상 더 합리적이라는 결론이 났다.
| 싸구려 책상, 좋은 의자 |
TDD를 할 때 어떤 물리적 환경이 적절한가? 나머지 시설은 싸구려를 쓸지라도 정말 좋은 의자를 구해라.
허리가 아프면 프로그램을 잘 짤 수가 없다.
27장. 테스팅 패턴
| 자식 테스트 |
지나치게 큰 테스트 케이스를 어떻게 돌아가도록 할 수 있을까? 원래 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고 그 작은 테스트 케이스가 실행되도록 하라. 그 후에 다시 원래의 큰 테스트 케이스를 추가하라.
빨강/초록/리팩토링 리듬은 성공이 지속되는 데 너무나도 중요해서, 그 리듬을 잃어버릴 것 같은 위기 순간에 부가의 노력으로 리듬을 유지하는 것이 충분히 가치 있다. 나의 경우, 테스트를 만들어놓고 보니 막상 이걸 통과시키려면 몇 가지를 한번에 수정해야만 하는 때에 이런 위기의 순간이 생기는데, 단 10분간만 빨간색이 지속되어도 겁이 난다.
나는 큰 테스트를 작성하고 나면 우선 교훈을 찾기 위해 노력한다. 왜 그렇게 테스트가 컸을까? 어떤 다른 방식을 취했더라면 좀더 작게 만들 수 있었을까?
나 자신을 형이상학적으로 바라볼 수 있게 되면, 거슬리는 테스트를 삭제하고 다시 시작한다. 때로는 진짜로 테스트를 지워버리기도하고, 또는 메서드 이름 앞에 x를 추가해서 실행만되지 않게 하기도 한다. 두 가지 모두 시도해 보기 바란다. 테스트 두 개가 깨진 상황에서는 어떤 다른 느낌이 드는지, 스스로 어떤 다른 방식으로 코딩하는지 관찰해보고 적절한 방식을 선택하라.
| 모의 객체 |
비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면 어떻게 해야 할까? 상수를 반환하게끔 만든 속임수 버전의 리소스를 만atabase들면 된다.
모의 객체(Mock Object)에 대해서는 최소한 책 한 권 분량 정도의 자료가 있지만, 이 책에서는 간단한 소개 정도만 하고자 한다. 고전적인 예는 데이터베이스다. 데이터베이스는 시작 시간이 오래 걸리고, 깨끗한 상태로 유지하기가 어렵다. 그리고 만약 데이터베이스가 원격 서버에 있다면 이로 인해 테스트 성공 여부가 네트워크 상의 물리적 위치에 영향을 받게된다. 또한 데이터베이스는 개발 중 많은 오류의 원인이 된다.
해법은 대부분의 경우에 진짜 데이터베이스를 사용하지 않는 것이다. 대다수의 테스트는, 마치 데이터베이스인 것처럼 행동하지만 실제로는 메모리에만 존재하는 객체를 통해 작성될 수 있다.
public void testOrderLookup() {
Database db = new MockDatabase();
db.expectQuery("select order_no from Order where cust_no is 123");
db.returnResult(new String[] {"Order 2", "Order 3"});
...
}
MockDatabase는 예상된 쿼리를 얻지 못하면 예외를 던질 것이다. 만약 쿼리가 올바르다면 MockDatabase는 상수 문자열에서 마치 결과 집합(result set)처럼 보이는 뭔가를 생성하여 반환한다.
성능과 견고함 이외에 모의 객체의 또 다른 가치는 가독성에 있다. 당신은 앞서 나온 테스트를 끝에서 끝까지 읽을 수 있다. 만약 사실적인 데이터로 가득 찬 테스트 데이터베이스를 사용한다면, 어떤 쿼리가 결과 14개를 되돌려야 한다고 적은 테스트를 보더라도 도대체 왜 14개가 올바른 답인지 알기가 쉽지 않다.
만약 모의 객체를 사용하길 원한다면, 값비싼 자원을 전역 변수에 손쉽게 저장해 버릴 수는 없다. 만약 그렇게 한다면, 전역 변수를 모의 객체로 설정하고, 테스트를 실행한 후 다시 전역 변수를 복구시켜 놓아야 한다.
모의 객체는 당신이 모든 객체의 가시성(visibility)에 대해 고민하도록 격려해서, 설계에서 커플링이 감소하도록 한다. 모의 객체를 사용하면 프로젝트에 위험 요소가 하나 추가된다. 모의 객체가 진짜 객체와 동일하게 동작하지 않으면 어떻게 될까? 모의 객체용 테스트 집합을 진짜 객체가 사용 가능해질 때 그대로 적용해서 이러한 위험을 줄일 수 있다.
| 셀프 션트(self shunt) |
한 객체가 다른 객체와 올바르게 대화하는지 테스트하려면 어떻게 할까? 테스트 대상이 되는 객체가 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 만들면 된다.
테스팅 사용자 인터페이스의 초록 막대를 동적으로 업데이트하고자하는 상황을 가정해 보자. UI 객체를 TestResult와 연결할 수 있다면 테스트가 실행된 시점, 테스트가 실패한 시점, 전체 테스트 슈트가 시작되고 끝난 시점 등을 통보 받을 수 있을 것이다. 그리고 이를 위한 테스트는 다음과 같을 것이다.
셀프 션트 패턴을 이용해 작성한 테스트가 그렇지 않은 테스트보다 읽기에 더 수월하다. 위의 테스트가 좋은 예다. 두 번째 버전의 테스트 메서드는 통보 횟수에 대한 두 값이 한곳에 모여 있는 반면, 첫 번째 버전의 테스트 메서드에서는 하나의 클래스에서 횟수를 0으로 설정하고 다른 클래스에서 1이 예상치임을 나타낸다.
셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스를 얻기 위해 인터페이스 추출(Extract Interface)을 해야 한다. (인터페이스를 추출하는 것이 더 쉬운지, 존재하는 클래스를 블랙 박스로 테스트하는 것이 더 쉬운지는 사용자의 몫이다) 셀프션트를 위해 추출해 낸 인터페이스는 여러 곳에서 쓰이는 경우가 많다.
자바의 경우, 셀프 션트를 사용한 결과로 인터페이스 안의 온갖 기괴한 메서드들을 다 구현한 테스트들을 보게 될 것이다. 낙관적 타입 시스템을 가진 언어(동적인 타입 검사를 수행하는 언어들)에서는 테스트 케이스 클래스가 실제로 테스트를 수행하는데 꼭 필요한 오퍼레이션들만 구현하면 된다. 하지만 자바에서는 빈 메서드라도 인터페이스의 모든 오퍼레이션들을 구현해야 한다. 그러므로 가능한 한 인터페이스를 작게 만들길 원할 것이다. 인터페이스에 대한 구현은 또한 적절한 값을 되돌리거나 부적절한 오퍼레이션이 호출된 경우 예외를 던지게끔 만들어야 할 것이다.
| 로그 문자열 |
메시지의 호출 순서가 올바른지를 검사하려면 어떻게 해야 할까? 로그 문자열을 가지고 있다가 메시지가 호출될 때마다 그 문자열에 추가하도록 한다.
xUnit에서 쓴 예제를 사용할 수 있다. setUp(), 테스트를 수행하는 메서드, tearDown()순서로 호출되길 원하는 템플릿 메서드(Template Method)가 있다. 각 메서드들이 로그 문자열에 자기 이름을 추가하게 구현하면 쉽게 읽히는 테스트를 만들 수 있다.
로그 문자열은 특히 옵저버(Observer)를 구현하고, 이벤트 통보가 원하는 순서대로 발생하는지를 확인하고자 할 때 유용하다. 만약 어떤 이벤트 통보들이 일어나는지를 검사하기는 하지만 그 순서는 상관이 없다면 문자열 집합을 저장하고 있다가 단언(assertion)에서 집합 비교를 수행하면 된다.
로그 문자열은 셀프 션트와도 잘 작동한다. 해당 테스트 케이스는 로그를 추가하고 적절한 값을 반환하는 식으로 셀프 션트한 인터페이스의 메서드를 구현한다.
| 크래시 테스트 더미 |
호출되지 않을 것 같은 에러 코드(발생하기 힘든 에러 상황)를 어떻게 테스트할 것인가? 실제 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수한 객체를 만들어서 이를 호출한다.
테스트되지 않은 코드는 작동하는 것이 아니다. 이것이 안전한 가정 같다. 그렇다면 수많은 에러 상황에 대해서는 어떻게 테스트할 것인가? --> 작동하길 원하는 부분에 대해서만 하면 된다.
파일 시스템에 여유 공간이 없을 경우 발생할 문제에 대해 테스트하기를 원한다고 생각해보자. 1)실제로 큰 파일을 많이 만들어서 파일 시스템을 꽉 채울 수도 있고, 2)가짜 구현(fake it)을 사용할 수도 있다.
그냥 시뮬레이션한다고 가정하자. 파일을 위한 크래시 테스트 더미(Crash Test Dummy)는 다음과 같다.
private class FullFile extends File {
public FullFile(String path) {
super(path);
}
public boolean createNewFile() throws IOException {
throw new IOException();
}
}
이제 다음과 같은 테스트를 할 수 있다.
public void testFileSystemError() {
File f = new FullFile("foo");
try {
saveAs(f);
fail();
} catch (IOException e) {
}
}
객체 전체를 흉내낼 필요가 없다는 점을 제외하면 크래시 테스트 더미는 모의 객체와 유사하다. 자바의 익명 클래스(anonymous innerclass)는 우리가 테스트하기 원하는 적절한 메서드만이 오류를 발생시키게끔 하기 위해 유용하게 쓰인다. 테스트 케이스 안에서 원하는 메서드 하나만 재정의할 수 있다. 이렇게 하면 테스트 읽기가 수월해진다.
public void testFileSystemError() {
File f = new File("foo") {
public boolean createNewFile() throws IOException {
throw new IOException();
}
};
try {
saveAs(f);
fail();
} catch (IOException e) {
}
}
| 깨진 테스트 |
혼자서 프로그래밍할 때 프로그래밍 세션을 어떤 상태로 끝마치는게 좋을까? 마지막 테스트가 깨진 상태로 끝마치는 것이 좋다.
프로그래밍 세션을 끝낼 때 테스트 케이스를 작성하고 이것이 실패하는 것을 확실히 확인하는 것이다. 나중에 다시 코딩하기위해 돌아왔을 때, 어느 작업부터 시작할 것인지 명백히 알 수 있다. 전에하고 있던 생각에 대한 명확하고 구체적인 책갈피를 가지게 되는 것이다. 깨진 테스트가 하나가 있다고 해서 프로그램 완성도가 더 떨어지는 것은 아니며, 단지 프로그램의 상태를 드러나게 해줄 뿐이다.
| 깨끗한 체크인 |
팀 프로그래밍을 할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까? 모든 테스트가 성공한 상태로 끝마치는 것이 좋다.
같은 팀원들과 함께 작업하는 경우라면 상황은 완전히 달라진다. 팀 프로젝트에서 프로그래밍 세션을 시작하는 경우라면 자신이 마지막으로 코딩한 다음부터 지금까지 무슨 일이 있었는지 세밀하게 알 수 없다. 안심이되고 확신이 서는 상태에서 시작할 필요가 있다. 따라서 코드를 체크인하기 전에 항상 모든 테스트가 돌아가는 상태로 만들어 두어야 한다.
체크인하기 전에 실행하는 테스트 스위트(test suite)는 작업 중에 분 단위로 실행하는 테스트 슈트보다 더 클 것이다. 때론 통합 테스트 슈트에서 테스트가 실패하는 경우도 있을 것이다. 그럴 땐 어떻게 해야 할까?
가장 단순한 규칙은 그동안 작업한 코드를 날려버리고 다시 하는 것이다. 실패한 테스트는 방금 만들어낸 프로그램을 완전히 이해하지 못했다는 강력한 증거이기 때문이다. 만약 전체 팀원이 규칙을 따른다면 체크인을 더 자주하려는 경향이 생길 것이다. 왜냐하면 제일 먼저 체크인하는 사람은 작업을 날릴 위험이 없을 테니까. 체크인을 자주하는 것은 아마 좋은 일일 것이다.
이 방법보다 아주 약간 방탕해 보이는 접근(원문은 뭐라고 적혀있었을까..?)은, 문제를 수정하고 테스트를 다시 실행해보는 것이다. 통합 자원을 독차지하는 것을 피하기 위해, 아마도 몇 분 후에는 그냥 포기해버리고 다시 시작해야 할 것이다. 말할 필요도 없지만, 테스트 슈트를 통과시키기 위해 주석 처리 하는 것은 금지되는 것이다.
테스트 코드를 실행하는데 다른 테스트코드는 괜찮았는데 저 테스트 클래스만 JUnit Vintage라는 오류가 발생했다. 이 문제를 해결해보고자 구글링을 해보았다. 먼저 JUnit5를 잘못 사용한 것이 아닌가? 하는 의심이 들었다.(실은 크게 잘못 사용할만한 부분이 없었다) 테스트코드에 대한 기초지식도 없었을 때, Junit5는 JUnit Vintage + JUnit Jupiter이다라고 하면서 공부했던 적이있는데, 어쨋거나 jUnit5의 두 요소중 하나의 요소만 테스트에 성공했으니 junit Vintage의 요소를 제대로 못받아 온게 아닌가... 즉 의존성 설정부분에서 실수한 것이 있을 것 같다는 생각이 들었다. 아래의 링크를 참조해서 설정을 해보았다.
위와 같이 의존성을 추가적으로 설정했다. 윗 글의 필자에 의하면 어떤 오류가 발생한다고 하는데, 나는 이전에 발생했던 오류가 동일하게 발생했다. 분명 이전까지 잘 돌아가던 테스트 코드이기도 했고, 의존성 변경에도 변화가 없는걸로 보아, 이 방법은 해결방법이 아니라고 생각하였다.
위 글은 테스트 메서드에 @Test 를 안붙여서 나와 같은 오류가 발생했다고 하는데, 나는 분명 모든 테스트 메서드를 @Test 어노테이션을 붙였다. 이 오류 케이스도 나의 문제가 아니였다.
의외로 이 오류가 발생한 원인은 간단했다. 제대로 동작하는 다른 테스트 코드들이랑 처음부터 비교해봤더니 오류가 발생하는 테스트 클래스에 'public' (접근자)이 붙어있다는 차이점이 있었다. 이유는 잘 모르겠지만 일단 다른 테스트 클래스처럼 public을 지우고 테스트를 실행해보았다.
설계를 하다보면 종종 상위의 인터페이스나 상위 클래스는 외부에 public으로 노출을 시키되, 하위의 구체 클래스들은 외부에 노출시키지 않아야 하는 경우가 있다. 이는 정보 은닉을 위한 좋은 설계 방법 중 하나이다. 이를 위해서 보통 하위의 구체 클래스들을 package private class(JAVA에서 접근자 키워드가 없이 선언되는 클래스)로 선언한다. 이렇게 하면 같은 패키지 내에서는 해당 클래스에 접근할 수 있어도 다른 패키지에서는 package private 클래스를 접근할 수 없으므로 오직 인터페이스나 상위 클래스를 이용할 수 밖에 없다. 따라서 하위 클래스들의 변경이나 추가와 같은 유연성이 보장된다.
하지만 문제는 package private class에 대한 Unit Test를 작성하는 일은 쉽지 않다는 점이다. 일반적으로 제품 코드와 유닛 테스트 코드는 분리되는 것이 좋다. 제품 코드와 유닛 테스트 코드가 같은 곳, 즉 같은 패키지 내에 존재하면 제품의 출시를 위해서 유닛 테스트 코드를 골라내는 작업이 필요하다. 또 제품 코드를 이해하기 위해서 소스를 뒤적이다 보면 유닛 테스트 코드가 함께 섞여 있어서 불편함을 겪게 된다. 그래서 일반적으로 제품 코드와 유닛 테스트 코드를 분리하여 두는데, 이렇게 되면 제품 코드와 유닛 테스트 코드의 패키지가 분리되면서 package private인 요소들에 대해서는 유닛 테스트가 접근할 수 없게 된다. 이렇게 되면 간접적으로 해당 요소들을 테스트해야 하는데 이는 매우 번거로운 작업이 된다.
오류에 관련된 포스팅들을 보다가 제목이 눈에 끌려서 보게되었다. 테스트 코드를 작성할 때, 캡슐화를 하다보니면 private method, private variable이 존재하게 되는데... 이 메서드나 변수를 테스트할 때, 사용할 수 없어서 어떻게 테스트를 해야하는지에 대한 고민들이 많았다. (내가 조언받은 방법으로는 더 많이 코드를 쪼개서 역할을 분담하므로서 public 인자로 바꿔가는 것이었다)
이 방법이 좋은지에 대해서는 확신은 못하겠지만, "이런 방법도 있구나?" 정도로 생각하면 좋을 것 같다.