7장 트렐로 애플리케이션 테스트
이번 장에서 다룰 내용
- 수동 테스트와 비교하여 테스트 케이스를 작성하는 것이 어떤 의미를 갖는지 살펴보기
- 테스트 기반 개발에 대해 알아보기
- 예제 트렐로 애플리케이션에 테스트 케이스를 구현
- 테스트 케이스 작성과 실행을 위해 자스민(Jasmine)과 카르마(Karma)를 살펴보기
기본개념
- 상용화 직전, 상용화를 마친 직후에 심각한 버그를 발견했다면 긴급하게 해결하기 위해 얼마나 많은 시간을 소요해야 할까?
- 개발자와 개발팀이 철저한 테스트를 했는데도 발생하는 버그는 어떻게 테스트를 피해 갔을까?
- 코드를 재검토 하고 버그가 있는 시나리오를 발견
- 시나리오를 테스트 케이스에 추가
- 6개월 주기로 사이클 반복
테스트의 어려움
- 개발자가 자신의 로직에 추가의 코드를 작성하는 것이 어려운 이유는 테스팅 프레임워크를 설정하는 것이 너무 복잡했기 때문
- Angular는 의존성 주입을 통해 개발자가 테스트 코드를 쉽게 작성할 수 있도록 개선
- 모든 모듈이 느슨하게 결합되도록 하였고 프레임워크에서 런타임에 의존성을 주입하는 것을 지원하도록 설계
- 각각의 컴포넌트에 대해 독립적으로 의존성을 흉내내어 테스트하는 것이 가능
테스트 기반 개발
- 테스트 케이스를 작성하는 시기에 관련한 두가지 의견
- 테스트 주도 개발
- 코드를 구현할 때 테스트를 작성
- 테스트 주도 개발(test-driven development - TDD)
- 테스트를 먼저 작성한 다음 테스트를 통과하는 코드를 작성
- 코드가 테스트를 통과하면 리팩토링을 통해 코드를 최적화
- 테스트를 통해 코드가 깨지지 않았다는 것을 재확인
- 모든 경우를 생각해보게 되고 결과적으로 모든 경우를 안전하게 처리하는 코드를 작성
- 요구사항이 자주 변경되는 경우에는 큰 부담이 됨
- 요건이 변경될 때마다 테스트를 만들어야 하기 때문
- 코드를 구현할 때 테스트
- TDD와의 차이는 테스트 케이스를 기준으로 개발하지 않는다는 점
- 단점은 실패 극복의 기회를 놓칠수 있음
- 반면 요구사항이 변경되어도 추가 작업을 하지 않아도 됨
단위 테스트 vs 종단간 테스트
- Angular의 두가지 테스팅 기법
- 단위 테스트
- 종단간 테스트
단위 테스트
- 의존성과 상관없이 특정 코드의 정확성을 확인
- 단일 책임의 원칙을 따르는 클래스이거나 컴포넌트가 대상
- 클래스 내부에 있는 코드만 테스트, 해당 클래스 가진 의존성은 테스트 하지 않음
- 외부 웹서비스 같은 의존성이 있는 경우 해당 부분은 임의로 구현하고 클래스 자체의 논리를 테스트
- 클래스의 독립된 동작을 확인할 수 있으므로 구현된 논리에 대한 확신을 얻을 수 있음
- 단위 테스트의 주요 기능
- 단일코드 : 전체 모듈이 아닌 작은 코드에 대해서만 초점
- 신속성 : 빠르게 실행되고 신속하게 작성, 복잡하지 않아야 함
- 의존성 제거 : 테스트할 코드를 변경시킬 수 있는 의존성이 없어야 함
- 모든 코드 경로 확인 : 해당 코드에 대해서는 모든 부분을 포함
- 한 가지만 확인 : 한 번에 여러 개를 확인하기보다는 단 하나만을 확인
- 전체 기능보다 테스트할 코드에만 초점을 맞추다보니 명확하고 간결한 테스트 가능
종단간(end-to-end) 테스트
- 전체 시스템을 함께 테스트
- 코드의 각 라인에 대해 신경쓰지 않으며 코드 커버리지도 신경 쓰지 않음
- 최종 사용자가 동작하는 것을 기존으로 테스트 케이스를 작성
- 의존성 위에서 동작하는 방식에 중점
- 단위 테스트의 다음 단계로 종단간 테스트를 검토
- 사용자에게는 해당 코드가 웹서비스나 DB호출 또는 다른 코드들과 어떻게 동작하는지가 중요
- 종단간 테스트의 단점
- 느림 : 시스템 전체를 테스트하기 때문에 단위 테스트와 비교하여 상대적으로 느리고 작성하는데 오래 걸림
- 복잡 : 사용자 관점에서 전체적인 테스트에 중점으 두고 있기 때문에 단위 테스트와 비교해 복잡
테스트 구조
- 준비(Arrange) : 단위 테스트의 초기 상태를 작성, 테스트를 하기 위해 필요한 설정(클래스 초기화, 데이터 설정)
- 작동(Act) : 실제로 행동하고 시험을 치르는 단계, 클래스의 함수를 호출하거나 일부 프로퍼티를 변경
- 확인(Assert) : 검증 단계, act 단계에서 코드의 상태를 변경해야 한다면 assert 단계에서 상태 변경이 예상대로 되었는지 확인
모의(Mocking)
- 의존성을 임의로 구현하여 독립된 코드를 테스트하는 개념
- 대부분의 코드는 어떤 식으로든 외부 코드와 관련이 있기 때문에 단위 테스트에서 중요한 개념
- 코드와 코드로직에 대해 의존성에 영향을 받지 않고 테스트하고자 한다면 의존성을 모의할 필요가 있음
- 모의는 의존성에 더미 객체를 생성, 더미 객체를 코드에 전달
테스트 도구
- 자스민(Jasmine)과 카르마(Karma)
- Angular CLI를 사용하면 자동으로 의존성을 추가
- 테스트 작성을 위해 필요한 초기 설정, 샘플 테스트도 생성
자스민(Jasmine)
테스트 케이스를 작성하고 관리하는 여러 기능을 제공
- describe()
- 테스트 집합의 컨테이너 역할
- 자스민은 describe 함수를 테스트 케이스의 루트로 인식 -케이스 실행의 시작지점
- beforeEach()
- 테스트 케이스의 준비(arrange) 역할
- 테스트 케이스를 설정하는 필요한 공통 코드를 작성
- it()
- 코드에서 작동(act)하는 부분
- describe 함수 내에 여러개의 it 함수가 있음
- 별도의 단위 테스트로 동작
- expert()
- 확인(assert)하는 부분
- 테스트 케이스의 유효성을 검사
- 확인을 도와주는 매칭 함수를 가지고 있음
- toBe : assert에서 기대하는 값
- toContain : 반환 값에 특정 값이 포함되어 있는지 확인
- toBeLossThan, toBeGreaterThan : 값의 범위를 확인
카르마(Karma)
- 정확한 테스트 케이스를 실행하는 역할
- 테스크 케이스를 식별한 다음 실행하고 실행 결과를 보여주는 커맨드 창 도구
- 브라우저를 실행 한 다음 브라우저에서 테스트 케이스를 실행하는 도구
- 브라우저 타입을 선택, 실행할 테스트 케이스를 선택하는 등의 여러가지 작업이 가능
- 카르마 CLI 패키지가 함께 제공
자스민과 카르마 설치 및 설정
- package.json 파일에 다음과 같이 설정되어 있음
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
"@angular/cli": "~7.3.5",
"@angular/compiler-cli": "~7.2.0",
"@angular/language-service": "~7.2.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2"
}
설치
카르마 CLI
npm 전역 범위에 카르마 CLI를 설치
npm i -g karma-cli
다른 dev 의존성
카르마 CLI 외에도 karma, karmachrome-launcher, karma-jasmine, jasmine-core와 같은 의존성을 설치
npm i karma karmachrome-launcher karma-jasmine jasmine-core
package.json에 있는 devDependencies 섹션의 모든 의존성을 설치
자스민 타이핑
테스트 케이스도 Typescript로 작성하기 위해 자스민 Typescript 타이핑을 설치
npm i @types/jasmine
설정
- package.json을 확인하여 모든 의존성이 잘 설치되었는지 확인
- 루트 폴더에 Karma.conf.js 라는 파일 생성
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage/SampleTrelloApplication'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
- basePath : 카르마가 테스트 케이스를 찾기 시작할 기본 경로를 정의
- frameworks : 사용할 프레임워크 지정
- plugins : 실행을 위해 필요한 의존성의 배열
- reporters : 테스트 케이스의 실행 진행률을 어떻게 볼지 설정
- autoWatch : 파일을 감시하다가 변경사항이 생기면 테스트를 다시 실행할지 여부
- browsers : 어떤 브라우져에서 실행되야 하는지 설정
테스트 프로젝트의 폴더와 파일 구조
- 테스트 파일은 일반적으로 spec.ts 확장자를 부여
- homepage.component.ts => homepage.component.spec.ts
- Angular CLI를 사용하면 동일한 명명 규칙을 사용하여 컴포넌트 또는 서비스를 생성할 때 자동으로 테스트 파일을 만들어 줌
파이프용 테스트 작성하기
- 가장 쉬운 예제 중 하나
- 파이프가 외부 의존성을 가지지 않아서 모의 코드 걱정 없이 로직을 쉽게 테스트할 수 있음
- 이 예제에서는 하나의 파이프함수(사용자정의 정렬)가 있으므로 이 파이프에 대한 테스트 케이스 작성
새로운 파일 생성
- 사용자정의 정렬 코드가 있는 폴더에 테스트 파일 생성(customsort_pipe.spec.ts)
- 파이프 함수의 논리
- task 배열과 sort boolean 프로퍼티, 두 개의 파라미터를 기반으로 함
- sort boolean 프로퍼티를 기반으로 task 배열을 정렬할지 여부를 결정
- 정렬은 task 프로퍼티를 기반으로 함
- 단위 테스트 케이스의 목록
- sort 프로퍼티를 true로 하고 파이프 함수를 호출 했을 때의 task 배열 확인
- sort 프로퍼티를 false로 하고 파이프 함수를 호출 했을 때의 task 배열 확인
- 빈 task 배열을 넘겼을 때의 결과 확인
- 빈 title을 가진 task가 task 배열에 포함된 경우 결과 확인
- 동일한 title을 가진 task가 2개 이상 task 배열에 포함되었을 경우의 결과 확인
처음 두 개에 중점을 두어 테스트 케이스 작성
테스트 케이스 작성
- 테스트 케이스의 의존성을 가져오는 것이 첫 번째 단계
- 모든 테스트 케이스는 해당 컴포넌트/파이프/서비스와 모델 데이터에 대한 의존성을 가짐
- 이번 경우 사용자정의 파이프 클래스와 task 객체
import { CustomSort} from './custom-sort.pipe';
import { Task } from '../model/task';
describe('사용자 정의 정렬', () => {
let pipe: CustomSort;
let tasks: Task[];
beforeEach(() => {
pipe = new CustomSort();
tasks = [{ title: 'Basic', id: 0, subtask: [], taskheaderId: '1' },
{ title: 'Advance', id: 0, subtask: [], taskheaderId: '1' },
{ title: 'Complex', id: 0, subtask: [], taskheaderId: '1' }];
});
it('task 배열을 정렬함', () => {
const expectedTask: Task[] = [
{ title: 'Advance', id: 0, subtask: [], taskheaderId: '1' },
{ title: 'Basic', id: 0, subtask: [], taskheaderId: '1' },
{ title: 'Complex', id: 0, subtask: [], taskheaderId: '1' }]
expect(pipe.transform(tasks, true)).toEqual(expectedTask);
});
it('task 배열을 정렬하지 않음', () => {
const expectedTask: Task[] = [
{ title: 'Basic', id: 0, subtask: [], taskheaderId: '1' },
{ title: 'Advance', id: 0, subtask: [], taskheaderId: '1' },
{ title: 'Complex', id: 0, subtask: [], taskheaderId: '1' }]
expect(pipe.transform(tasks, false)).toEqual(expectedTask);
});
});
함수 설명
- describe 함수는 두 개의 파라미터를 사용
- 첫번째는 description으로 문자열로 테스트 케이스를 정의하는 설명
- 두번재는 자스민이 호출할 콜백 함수
beforeEach 함수
- 사용자정의 sort 객체를 초기화, transform 함수에 전달될 task 배열을 만듦
- it 함수가 호출되기 전에 호출
- 여러 테스트 케이스를 실행할 때 이전 테스트 케이스의 데이터가 다음 테스트 케이스의 데이터에 영향을 미치지 않도록 함
it 함수
- beforeEach 함수에서 만든 task 배열을 전달하고 배열이 제목별로 정렬되도록 함
- 예상되는 출력 배열을 정의한 다음 toEqual을 사용해 transform 함수에서 반환된 값과 비교
테스트 케이스 실행
- Angular CLI를 사용하면 터미널에서 아래 명령을 실행하면 카르마가 브라우저를 실행하고 애플리케이션의 모든 테스트 케이스를 실행
ng test
서비스 클래스를 위한 테스트 케이스 작성
- 다른 시스템에서 데이터를 가져오는 모든 프로젝트는 서비스 클래스를 가지고 있음
- 서비스 클래스는 웹서비스를 호출하기 위해 HTTP를 사용
- 따라서 서비스 클래스에서 단위 테스트를 작성하려면 HTTP와 서비스가 필요로 하는 다른 외부 의존성을 모의해야 함
새로운 파일 생성
- service 폴더에 trello.service.spec.ts 파일 생성
- 트렐로 서비스는 HttpClient GET 메서드를 사용하여 JSON 파일에서 보드 데이터를 가져오는 기능
- 프로미스를 사용하여 응답을 가져오고 성공 또는 실패를 프로미스에 반환
- 테스트에 필요한 두가지 흐름
- public 변수가 이미 보드에 있는지 확인
- 보드가 없을 때 HTTP를 호출
- 테스트 케이스
- 보드 객체가 없는 경우 HTTP 호출을 통해 데이터를 가져오는지 확인
- 이미 보드 객체가 있는 경우 해당 객체를 반환하는지 확인
테스트 케이스 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { TrelloService } from './trello.service';
import { of } from 'rxjs';
import { Board } from '../model/board';
describe('Trello HTTP Service', () => {
let trelloService: TrelloService;
let mockHTTP;
let fakeBoards: Board[];
beforeEach(() => {
mockHTTP = jasmine.createSpyObj('mockHTTP', ['get']);
trelloService = new TrelloService(mockHTTP);
});
it('보드 undefined 확인', () => {
mockHTTP.get.and.returnValue(of(fakeBoards));
trelloService.getBoardsWithPromises().then(boards => this.boards = boards);
expect(fakeBoards).toBeDefined();
});
it('보드 defined 확인', () => {
fakeBoards = new Array();
mockHTTP.get.and.returnValue(of(fakeBoards));
trelloService.getBoardsWithPromises().then(boards => this.boards = boards);
expect(fakeBoards).toBeDefined();
});
it('보드 가져오기', () => {
trelloService.Boards = new Array();
trelloService.Boards.push({
id: 0,
title: 'Test Board',
task: []
});
mockHTTP.get.and.returnValue(of(trelloService.Boards));
trelloService.getBoardsWithPromises().then(boards => {
fakeBoards = boards;
expect(fakeBoards).toBeDefined();
expect(fakeBoards[0].title).toEqual('Test Board');
});
});
});
describe 함수
describe('Trello HTTP Service', () => {
let trelloService: TrelloService;
let mockHTTP;
let fakeBoards: Board[];
});
- 트렐로 서비스를 위한 fakeBoards라는 보드 배열을 만듦
- fakeBoards는 트렐로 서비스의 getBoardsWithPromises에서 반환 하는 값
- 트렐로 서비스 클래스 의존성(HttpClient)을 모의
- 프로퍼티를 선언만 하고 초기화는 beforeEach 함수 내에서 수행
beforeEach 함수
1
2
3
4
beforeEach(() => {
mockHTTP = jasmine.createSpyObj('mockHTTP', ['get']);
trelloService = new TrelloService(mockHTTP);
});
- HttpClient 객체를 모의 하여 트렐로 서비스 클래스의 인트턴스를 만듦
- 2행
- HttpClient 객체를 모의하려면 내부 메서드를 모의하는 방법이 필요
- 자스민에서 제공하는 createSpyObj 함수를 사용
- 기본적으로 정의하는 티입의 더미 객체를 생성 또는 더미 함수 배열을 제공하여 메서드로 사용할 수 있게 함
- mockHTTP를 사용하면 get 메서드를 찾을 수 있음
- 3행 : 트렐로 서비스 클래스의 인스턴스를 만들고 mockHTTP 객체를 전달
it 함수
- json 객체가 없는지 확인
- 보드 객체가 undefined 인 경우 HTTP 호출
1
2
3
4
5
it('보드 undefined 확인', () => {
mockHTTP.get.and.returnValue(of(fakeBoards));
trelloService.getBoardsWithPromises().then(boards => this.boards = boards);
expect(fakeBoards).toBeUndefined();
});
- 2열
- 자스민에서 get 함수를 호출하면 Observable 형태의 보드 배열 타입을 반환한다고 알려줌
- it 함수에서 getBoardsWithPromises 함수를 호출하면 자스민은 HTTP 호출에서 보드 배열의 Observable을 반환하는지 확인
- 4열
- 아직 배열을 초기화 하지 않았기 때문에 반환값은 undefined
- expect 문에서 테스트
- fakeBoards 객체를 초기화 했다면 반환 값은 undefined가 아닌 해당 값을 받음
- 그럼 expect문은 undefined가 아닌 보드 배열 객체를 받음
- 자스민을 사용하면 객체를 모의할 뿐만 아니라 단위 테스트에서 사용하는 반환 값 또한 모의된 객체로 가져올 수 있음
it('보드 defined 확인', () => {
fakeBoards = new Array();
mockHTTP.get.and.returnValue(of(fakeBoards));
trelloService.getBoardsWithPromises().then(boards => this.boards = boards);
expect(fakeBoards).toBeDefined();
});
- 보드 객체를 트렐로 서비스의 프로퍼티에 할당
- 예상되는 동작은 getBoardsWithPromises 함수의 else 부분
it('보드 가져오기', () => {
trelloService.Boards = new Array();
trelloService.Boards.push({
id: 0,
title: 'Test Board',
task: []
});
mockHTTP.get.and.returnValue(of(trelloService.Boards));
trelloService.getBoardsWithPromises().then(boards => {
fakeBoards = boards;
expect(fakeBoards).toBeDefined();
expect(fakeBoards[0].title).toEqual('Test Board');
});
});
- 새 보드 객체 배열을 만들고 이를 트렐로 서비스의 보드 객체에 할당
- 함수 호출
- expect문을 통해 프로미스 함수 내에서 올바른 객체를 받았는지 확인
- expect문을 then 안에 있는 것은 프로미스가 성공적으로 수행된 뒤에만 expect로 값을 비교하려고 했기 때문
- expect문이 then 밖에 있는다면 결과를 받기 전에 실행됨으로 언제나 실패하게 됨
독립된 컴포넌트의 테스트 케이스 작성
- 서비스나 파이프와 비교해 컴포넌트의 고유한 점은 사용자 인터페이스와 상호 작용한다는 점
- 컴포넌트를 테스트할 때는 다음과 같은 두 가지 전력 사용 가능
- 테스트 컴포넌트를 격리
- 서비스나 파이프와 비슷한 방식으로 컴포넌트를 테스트
- 컴포넌트의 템플릿 부분을 고려하지 않고 테스트 하지도 않음
- 컴포넌트에 있는 논리를 테스트하려는 경우에 유용
- 컴포넌트에 대한 포괄적인 검증을 못할 수도 있음
- 종단간 컴포넌트 테스트
- 템플릿과 함께 컴포넌트를 테스트
- 컴포넌트 논리에서 발생하는 변경 사항이 템플릿에 미치는 영향을 테스트할 수 있음
- 테스트 컴포넌트를 격리
- 이번 섹션은 첫 번째 방식으로 테스트
새로운 파일 생성
- board 폴더에 테스트 파일 생성
- 보드 컴포넌트는 새 작업 추가, 보드 제목 수정, 기존 작업 수정과 같은 많은 기능이 있음
- 새 작업 추가 기능에 중점을 두고 테스트 케이스 작성
테스트 케이스 작성
- addtask 메서드는 이미 존재하는 작업의 전체 길이를 가져와 1씩 증가시켜서 새로운 작업에 대한 id로 부여
- 그 다음 이 새 작업을 보드 객체의 기존 작업 목록에 추가
- 두 가지 테스트를 작성
- 보드 객체에 첫 번째 작업을 추가
- 이미 존재하는 목록에 작업을 추가
- 격리된 테스트 케이스를 생성할 것이기 때문에 객체를 생성하려는 클래스만 import
import { BoardComponent } from './board.component';
import { Board } from '../model/board'
테스트 케이스 구현
- 보드 컴포넌트의 의존성 목록
- HTML 엘리먼트를 참조하는 엘리먼트 참조
- ActivatedRoute로 라우팅 값 가져오기
- TrelloService 서비스로 보드 객체 가져오고 업데이트 하기
-
보드 컴포넌트를 효과적으로 테스트 하려면 이 모든 것을 테스트
- before 함수에서 보드 컴포넌트의 인스턴트를 만들고 모의 종속성을 코드에 전달
describe('BoardComponent', () => {
let boardComponent: BoardComponent;
let mockElementRef, mockRoute, mockTrelloService;
beforeEach(() => {
boardComponent = new BoardComponent(mockElementRef, mockRoute, mockTrelloService);
});
});
- 두 가지 테스트 케이스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
it('기존 작업에 새 작업 추가 테스트', () => {
boardComponent.addtaskText = 'Test task';
boardComponent.board = new Board();
boardComponent.board.id = 1;
boardComponent.board.title = '보드 1';
boardComponent.board.task = new Array();
boardComponent.board.task.push({
id: 1,
title: '작업1',
subtask: [],
taskheaderId: '1'
});
boardComponent.addtask();
expect(boardComponent.board.task.length).toBe(2);
});
it('첫 번째 작업 추가 테스트', () => {
boardComponent.addtaskText = '더미';
boardComponent.board = new Board();
boardComponent.board.id = 1;
boardComponent.board.title = '보드 1';
boardComponent.board.task = new Array();
boardComponent.addtask();
expect(boardComponent.board.task.length).toBe(1);
expect(boardComponent.board.task[0].id).toBe(1);
expect(boardComponent.board.task[0].title).toBe('더미');
});
- 1행 : 첫 번째 테스트
- 기존 보드 객체를 초기화, 새로운 작업으로 정의 후 addtask 함수에 전달
- expect 함수는 작업 배열의 길이만 확인
- 17행 : 두 번째 테스트
- 보드에 기존 작업이 없고 새로운 작업을 생성한다고 가정
- 테스트 케이스의 길이가 1인 확인 하는 것을 제외하고는 첫 번째 케이스와 유사
- 첫 번째 테스트 케이스르 작성하는 경우 코드에 예외가 발생하지 않는지 확인
- 여러 값을 확인하기 위해 여러개의 expert문이 있음
통합 컴포넌트를 위한 테스트 케이스 작성
- 통합 테스트를 구현할 때 가장 복잡한 것은 설정 부분
- 컴포넌트와 의존성의 초기화를 함께 해야함
- 복잡한 통합 테스트의 몇 가지 이점
- 바인팅 테스트 가능, 복잡한 바인딩이 있는 경우 매우 유용
- 테스트를 작성하며 바로 문제를 식별
- 통합 테스트를 통해 템플릿 관련 버그를 신속하고 효율적으로 식별하고 사전에 수정
통합테스트 설정
- 컴포넌트 인스턴스가 필요하지 않음
- 연관된 템플릿이 있고 컴포넌트를 로드하고 바인딩을 테스트 할 수 있도록 모듈에 포함된 컴포넌트가 필요
- homepage 컴포넌트에 대한 통합 테스트 케이스를 작성
- 테스트 케이스에 필요한 import
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
- Angular는 angular/core/testing 모듈에 특수한 라이브러리 제공
- TestBed
- Angular가 컴포넌트를 만드는데 사용
- 템플릿을 생성하고 이를 각 컴포넌트와 연관지음
- 테스트 중인 컴포넌트를 보유하고 있는 모듈을 생성
- async
- beforeEach 메서드에서 비동기 작업을 동기 작업으로 변환하는데 사용하는 도우미 함수
- ComponentFixture
- ComponentFixture는 테스트 함수에서 하용하는 타입
- TestBed를 사용하여 컴포넌트를 만들면 ComponentFixture 타입의 컴포넌트를 반환
- TestBed
- 홈페이지 컴포넌트는 두 가지 서비스에 의존
- 트랠로 서비스
- 라우터 서비스
constructor(public el: ElementRef, private _route: ActivatedRoute, private _trelloService: TrelloService) { }
- 다음과 같이 import를 완성
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { HomepageComponent } from './homepage.component';
import { TrelloService } from './../service/trello.service';
테스트 케이스 작성
- 홈페이지 컴포넌트의 기능
- ngOninit에서 사용 가능한 모든 보드를 가져와서 로컬 보드 개열 객체에 바인딩
- 새로운 보드 기능을 추가
- 작업은 없고 제목은 ‘New Board’로 설정
- 세 가지 테스트 케이스를 작성
- 컴포넌트가 성공적으로 생성되었는지 확인
- 두 개의 보드를 추가했을 때 컴포넌트에 보드가 잘 추가되고 템플릿에 표시가 되는지 확인
- 새 보드를 만들고 UI에 잘 표시되는지 확인
- 통합 테스트에서는 바인딩이 잘 동작되는지 보기 위해 UI도 확인 가능
- 컴포넌트의 인스턴스를 얻기 위해 추가 작업 필요
describe 함수
1
2
3
4
describe('HomepageComponent', () => {
let component: HomepageComponent;
let fixture: ComponentFixture<HomepageComponent>;
});
- 2행 : 홈페이지 컴포넌트를 정의
- 3행
- fixture 변수를 선언, 홈페이지 컴포넌트를 타입으로 전달
- ComponentFixture는 기본적으로 컴포넌트의 래퍼(warpper)로 컴포넌트의 인스턴스를 직접 만든었다면 제공하지 않을 추가 기능을 제공
- 변경 탐지나 컴포넌트 템플릿의 html 엘리먼트 접근
첫 번째 beforeEach 함수
- beforeEach 함수는 컴포넌트와 모듈을 생성하는 장소, 또한 의존성과 동작을 정의
- 통합테스트의 경우 일반적으로 두 가지 beforeEach 함수가 있음
- 하나는 모듈을 초기화, 다른 하나는 컴포넌트를 만드는 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { APP_BASE_HREF } from '@angular/common';
import { of } from 'rxjs';
...
describe('HomepageComponent', () => {
...
beforeEach(async(() => {
const mockTrelloService = { getBoardsWithPromises: () => of([]).toPromise() };
TestBed.configureTestingModule({
declarations: [HomepageComponent],
imports: [RouterModule.forRoot([])],
providers: [{ provide: APP_BASE_HREF, useValue: '/' },
{ provide: TrelloService, useValue: mockTrelloService }]
}).compileComponents();
}));
...
});
...
- 8행
- TestBed 유틸리티의 함수를 사용하여 모듈을 생성
- configureTestingModule 함수는 모듈을 생성하고 모둘에 대한 의존성을 정의
- app.module 파일의 ngModule과 비슷한 모듈을 생성
- 다음과 같은 프로퍼티를 가짐
- declarations : 모듈과 관련된 컴포넌트를 정의
- imports : 의존성이 있는 모듈을 가져오기 위한 import 구문, 라우트 객체만으로 컴포넌트를 만족시키므로 빈 라우트를 추가
- providers : 서비스와 기본 href 링크와 같은 외부 의존성을 정의하는 공급자, 두개의 속성이 있음
- provide : 어떤 의존성이 필요한지 알려줌
- useValue : 이 의존성을 대체할 객체 또는 값
- configureTestingModule 함수는 비동기 함수이지만 테스트 케이스가 실행되기 전에 모듈이 생성되었는지 확인하기 위해 동기적으로 실행
- async 함수를 사용하면 비동기를 동기식 연산으로 변환
- 7행
- 원래의 서비스를 대체할 mockTrelloService 객체를 생성
- 이 서비스 클래스는 getBoardsWithPromises라는 하나의 메서드를 사용
- 이 메서드는 보드 타입에 대한 프로미스를 반환
- 홈페이지 컴포넌트가 mockTrelloService에서 이 메서드를 호출할 때 보드 타입의 프로미스를 반환했는지 확인하기 위해 반환 값으로 빈 배열을 할당
- 13행
- compileComponents 함수는 템플릿이 있는 컴포넌트로 컴파일하고 해당 컴포넌트를 모듈에 추가
두 번째 beforeEach 함수
- 컴포넌트를 만들고 인스턴스를 가져오는 것
beforeEach(() => {
fixture = TestBed.createComponent(HomepageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
- 홈페이지 컴포넌트를 생성하고 이를 컴포넌트 fixture에 할당
- fixture에서 컴포넌트인스턴스를 component 변수에 할당
- 컴포넌트 fixture에서 detectChanges를 호출
- 테스트 하는 동안에 컴포넌트의 UI를 변경하기 위한 코드를 사용했다면 항상 detectChanges 메서드를 호출
if 함수
- 첫 번재는 컴포넌트를 성공적으로 만들 수 있는지 확인
it('컴포넌트 생성 확인', () => {
expect(component).toBeTruthy();
});
- toBeTruthy 메서드를 사용하여 컴포넌트가 있는지 확인
- 통합 테스트를 작성할 때마다 항상 이 테스트 케이스를 실행
- 생성한 컴포넌트 인스턴스가 잘 생성되었고 누락된 의존성이 있는지 확인하는 역할
UI 보드에서 확인
- 컴포넌트에서 템플릿까지 종단간 테스트를 하는 방법
- 두 개의 보드가 있을 때 UI에 잘 표시되는지 컴포넌트 프로퍼티는 정상인지 확인
it('두 개의 보드가 있는지 확인', () => {
component.boards = new Array();
component.boards.push(
{
id: 1,
task: [],
title: 'Board 1'
},
{
id: 2,
task: [],
title: 'Board 2'
}
);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
const title = compiled.querySelectorAll('.title');
expect(title[0].textContent).toContain('Board 1');
expect(title[1].textContent).toContain('Board 2');
expect(component.boards.length).toBe(2);
});
- 컴포넌트 객체에 보드 배열을 만들고 두 개의 보드를 생성
- 두 개의 보드를 UI에 반영하기 위해 detectChanges 메서드를 호출
- detectChanges 메서드는 Angular를 호출하여 모든 프로퍼티에 대한 바인딩을 갱신
- 컴포넌트 fixture 객체의 함수를 사용하여 템플릿에서 엘리먼트를 가져옴
- debugElement는 컴포넌트 템플릿과 연관된 루트 엘리먼트
- nativeElement는 템플릿에 대한 핸들을 제공
- 템플릿에 대한 핸들을 확보하고 나면 표준 UI API를 호출하여 엘리먼트를 가져오고 그 값을 내부적으로 확인 가능
- html 엘리먼트가 title 클래스 프로퍼티를 가졌는지
- 두 개의 보드를 잘 가져왔는지 확인
- 마지막 라인에서 컴포넌트 클래스의 보드 객체가 두 개의 보드를 가지고 있는지 확인
신규 보드 생성 테스트
it('새 보드 생성', () => {
component.addBoard();
fixture.detectChanges();
expect(component.boards.length).toBe(1);
const compiled = fixture.debugElement.nativeElement;
const title = compiled.querySelectorAll('.title');
expect(title.length).toBe(2);
expect(title[0].textContent).toContain('New Board');
});
- 컴포넌트에서 addBoard 메서드를 호출한 다음 Angular가 바인딩을 새로 고치도록 하기 위해 detectChanges 메서드를 호출
- 정상적으로 완료 되면 expect 문으로 보드의 수가 한개인지 title이 있는 두 개의 엘리먼트가 있는지 확인
- 마지막 행에서 첫 번째 새로운 보드가 New Board라는 컨텐츠를 가졌는지 확인