시작하기 전에
팁: 이 문서는 컴포넌트 하네스 개요를 읽었다고 가정합니다. 컴포넌트 하네스를 처음 사용한다면 먼저 해당 문서를 읽어보세요.
CDK 설치하기
Component Dev Kit(CDK)는 컴포넌트의 동작을 구현하는 기본 요소 모음입니다.
컴포넌트 하네스를 사용하려면 먼저 npm에서 @angular/cdk를 설치해야 합니다.
터미널에서 Angular CLI로 설치할 수 있습니다.
ng add @angular/cdk
테스트 하네스 환경과 로더
컴포넌트 테스트 하네스는 여러 테스트 환경에서 사용할 수 있습니다. Angular CDK는 두 가지 환경을 기본으로 지원합니다.
각 환경은 하네스 로더를 제공합니다. 로더는 테스트에서 사용할 하네스 인스턴스를 생성합니다. 지원하는 테스트 환경별 사용 방법은 아래 내용을 참고하세요.
다른 테스트 환경을 사용하려면 별도의 연결 구현이 필요합니다. 자세한 내용은 추가 테스트 환경에서 하네스 지원하기를 참고하세요.
단위 테스트에서 TestbedHarnessEnvironment 로더 사용하기
단위 테스트에서는 TestbedHarnessEnvironment로 하네스 로더를 만들 수 있습니다.
이 환경은 Angular TestBed로 생성한 컴포넌트 픽스처를 사용합니다.
픽스처의 최상위 엘리먼트를 기준으로 하네스 로더를 만들려면 loader() 메서드를 사용하세요.
const fixture = TestBed.createComponent(MyComponent);
// 픽스처에서 하네스 로더를 생성합니다
const loader = TestbedHarnessEnvironment.loader(fixture);
...
// 로더로 하네스 인스턴스를 가져옵니다
const myComponentHarness = await loader.getHarness(MyComponent);
픽스처 바깥에 있는 엘리먼트의 하네스를 가져올 로더를 만들려면 documentRootLoader() 메서드를 사용하세요.
예를 들어 떠 있는 엘리먼트나 팝업을 표시하는 코드는 DOM 엘리먼트를 문서의 body에 직접 추가하는 경우가 많습니다.
Angular CDK의 Overlay 서비스가 이에 해당합니다.
픽스처의 최상위 엘리먼트에 있는 하네스는 harnessForFixture()로 직접 생성할 수도 있습니다.
E2E 테스트에서 SeleniumWebDriverHarnessEnvironment 로더 사용하기
WebDriver 기반 E2E 테스트에서는 SeleniumWebDriverHarnessEnvironment로 하네스 로더를 만들 수 있습니다.
loader() 메서드로 현재 HTML 문서의 최상위 엘리먼트를 기준으로 하는 하네스 로더 인스턴스를 가져오세요.
이 환경은 WebDriver 클라이언트를 사용합니다.
let wd: webdriver.WebDriver = getMyWebDriverClient();
const loader = SeleniumWebDriverHarnessEnvironment.loader(wd);
...
const myComponentHarness = await loader.getHarness(MyComponent);
하네스 로더 사용하기
하네스 로더 인스턴스는 특정 DOM 엘리먼트에 대응하며, 해당 엘리먼트 아래에 있는 엘리먼트의 컴포넌트 하네스 인스턴스를 생성합니다.
첫 번째 엘리먼트의 ComponentHarness를 가져오려면 getHarness() 메서드를 사용하세요.
모든 ComponentHarness 인스턴스를 가져오려면 getAllHarnesses() 메서드를 사용합니다.
// 첫 번째 엘리먼트의 하네스를 가져옵니다
const myComponentHarness = await loader.getHarness(MyComponent);
// 모든 엘리먼트의 하네스를 가져옵니다
const myComponentHarnesses = await loader.getAllHarnesses(MyComponent);
HarnessLoader에는 getHarness, getAllHarnesses 외에도 하네스를 조회하는 유용한 메서드가 있습니다.
getHarnessAtIndex(...): 주어진 조건과 일치하는 컴포넌트 중 특정 인덱스에 있는 컴포넌트의 하네스를 가져옵니다.countHarnesses(...): 주어진 조건과 일치하는 컴포넌트 인스턴스의 개수를 구합니다.hasHarness(...): 주어진 조건과 일치하는 컴포넌트 인스턴스가 하나 이상 있는지 확인합니다.
예를 들어 클릭하면 대화상자를 여는 재사용 가능한 대화상자 버튼 컴포넌트를 생각해 봅시다. 다음 컴포넌트로 구성되며 각각에 대응하는 하네스가 있습니다.
MyDialogButton:MyButton과MyDialog를 조합하고 편리한 API를 제공합니다.MyButton: 일반적인 버튼 컴포넌트입니다.MyDialog: 클릭했을 때MyDialogButton이document.body에 추가하는 대화상자입니다.
다음 테스트는 각 컴포넌트의 하네스를 로드합니다.
let fixture: ComponentFixture<MyDialogButton>;
let loader: HarnessLoader;
let rootLoader: HarnessLoader;
beforeEach(() => {
fixture = TestBed.createComponent(MyDialogButton);
loader = TestbedHarnessEnvironment.loader(fixture);
rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});
it('loads harnesses', async () => {
// `harnessForFixture`로 부트스트랩된 컴포넌트의 하네스를 로드합니다
dialogButtonHarness = await TestbedHarnessEnvironment.harnessForFixture(
fixture,
MyDialogButtonHarness,
);
// 버튼 엘리먼트가 픽스처의 최상위 엘리먼트 안에 있으므로 `loader`를 사용합니다.
const buttonHarness = await loader.getHarness(MyButtonHarness);
// 버튼을 클릭해서 대화상자를 엽니다
await buttonHarness.click();
// 대화상자는 픽스처의 최상위 엘리먼트 바깥인 `document.body`에 추가되므로
// 이 경우에는 `rootLoader`를 사용합니다.
const dialogHarness = await rootLoader.getHarness(MyDialogHarness);
// ... 결과를 검증합니다
});
환경에 따른 하네스 동작
하네스가 모든 환경에서 완전히 똑같이 동작하지는 않을 수 있습니다. 실제 사용자 상호작용과 단위 테스트에서 발생시키는 가상 이벤트 사이에는 피할 수 없는 차이가 있습니다. Angular CDK는 가능한 범위에서 동작을 일관되게 맞춥니다.
자식 엘리먼트와 상호작용하기
하네스 로더의 최상위 엘리먼트 아래에 있는 엘리먼트와 상호작용하려면 자식 엘리먼트의 HarnessLoader 인스턴스를 사용하세요.
첫 번째 자식 엘리먼트에는 getChildLoader(), 모든 자식 엘리먼트에는 getAllChildLoaders() 메서드를 사용합니다.
const myComponentHarness = await loader.getHarness(MyComponent);
// '.child' 셀렉터와 일치하는 첫 번째 자식 엘리먼트의 로더를 가져옵니다
const childLoader = await loader.getChildLoader('.child');
// '.child' 셀렉터와 일치하는 모든 자식 엘리먼트의 로더를 가져옵니다
const allChildLoaders = await loader.getAllChildLoaders('.child');
하네스 필터링하기
페이지에 같은 컴포넌트의 인스턴스가 여러 개 있으면 컴포넌트 프로퍼티를 기준으로 필터링해서 특정 인스턴스를 가져와야 할 수 있습니다.
이때 하네스 조건식을 사용할 수 있습니다.
하네스 조건식은 ComponentHarness 클래스와 컴포넌트 인스턴스를 필터링하는 조건식 함수를 연결하는 클래스입니다.
HarnessLoader에 하네스를 요청할 때 실제로 전달하는 것은 HarnessQuery입니다.
쿼리는 다음 두 가지 중 하나입니다.
- 하네스 생성자: 해당 하네스를 가져옵니다.
HarnessPredicate: 하나 이상의 조건으로 필터링한 하네스를 가져옵니다.
HarnessPredicate는 ComponentHarness를 상속하는 모든 클래스에 사용할 수 있는 기본 필터(selector, ancestor)를 지원합니다.
// 하네스 조건식으로 MyButtonComponentHarness를 로드하는 예제
const disabledButtonPredicate = new HarnessPredicate(MyButtonComponentHarness, {
selector: '[disabled]',
});
const disabledButton = await loader.getHarness(disabledButtonPredicate);
하지만 보통 하네스에는 컴포넌트별 필터 옵션을 받고 HarnessPredicate를 반환하는 with() 정적 메서드를 구현합니다.
// 특정 셀렉터로 MyButtonComponentHarness를 로드하는 예제
const button = await loader.getHarness(MyButtonComponentHarness.with({selector: 'btn'}));
추가 필터 옵션은 하네스 구현마다 다르므로 자세한 내용은 각 하네스 문서를 참고하세요.
테스트 하네스 API 사용하기
각 하네스는 해당 컴포넌트에 맞는 API를 정의하지만, 모두 ComponentHarness라는 공통 기본 클래스를 사용합니다.
이 기본 클래스의 hostSelector 정적 프로퍼티는 하네스 클래스와 DOM의 컴포넌트 인스턴스를 연결합니다.
그 외의 하네스 API는 해당 컴포넌트마다 다릅니다. 특정 하네스의 사용 방법은 컴포넌트 문서를 참고하세요.
예를 들어 다음은 Angular Material 슬라이더 컴포넌트 하네스를 사용하는 컴포넌트 테스트입니다.
it('should get value of slider thumb', async () => {
const slider = await loader.getHarness(MatSliderHarness);
const thumb = await slider.getEndThumb();
expect(await thumb.getValue()).toBe(50);
});
Angular 변경 감지와 연동하기
테스트 하네스는 기본으로 DOM 엘리먼트의 상태를 읽기 전과 DOM 엘리먼트와 상호작용한 후에 Angular 변경 감지를 실행합니다.
비동기 작업이 진행 중일 때 컴포넌트 상태를 확인하는 등, 테스트에서 변경 감지를 더 세밀하게 제어해야 할 수 있습니다.
이때는 manualChangeDetection 함수로 특정 코드 블록의 자동 변경 감지 처리를 비활성화하세요.
it('checks state while async action is in progress', async () => {
const buttonHarness = await loader.getHarness(MyButtonHarness);
await manualChangeDetection(async () => {
await buttonHarness.click();
fixture.detectChanges();
// 비동기 클릭 동작이 진행 중일 때 예상한 상태인지 확인합니다.
expect(isProgressSpinnerVisible()).toBe(true);
await fixture.whenStable();
// 비동기 클릭 동작이 끝난 후 예상한 상태인지 확인합니다.
expect(isProgressSpinnerVisible()).toBe(false);
});
});
거의 모든 하네스 메서드는 다음 목적을 위해 비동기로 동작하고 Promise를 반환합니다.
- 단위 테스트 지원
- E2E 테스트 지원
- 비동기 동작이 변경되어도 테스트가 영향을 받지 않도록 보호
Angular 팀은 테스트의 가독성을 높이기 위해 await 사용을 권장합니다.
await를 사용하면 해당 Promise가 이행될 때까지 테스트 실행을 기다립니다.
각 동작을 순서대로 실행하지 않고 여러 동작을 동시에 실행한 뒤 모두 끝날 때까지 기다려야 할 때도 있습니다.
컴포넌트 하나의 여러 프로퍼티를 읽는 경우가 예입니다.
이때는 parallel 함수로 작업을 병렬 실행하세요.
이 함수는 Promise.all과 비슷하게 동작하면서 변경 감지 검사도 최적화합니다.
it('reads properties in parallel', async () => {
const checkboxHarness = await loader.getHarness(MyCheckboxHarness);
// checked와 intermediate 프로퍼티를 동시에 읽습니다.
const [checked, indeterminate] = await parallel(() => [
checkboxHarness.isChecked(),
checkboxHarness.isIndeterminate(),
]);
expect(checked).toBe(false);
expect(indeterminate).toBe(true);
});