[Angular 마스터하기] Day 18 - 테스팅 기초, 안전한 코드 만들기
이제와서 시작하는 Angular 마스터하기 - Day 18 “테스트로 안정적인 코드를 작성하세요! ✅”
오늘 배울 내용
- Angular 테스트의 기본 구조
- standalone 컴포넌트 테스트
- signals 상태 테스트
- 서비스와 HTTP 테스트
- 테스트를 어디까지 작성할지 판단하는 기준
1. Angular 테스트의 기본 흐름
Angular 테스트는 보통 TestBed로 테스트용 모듈 환경을 만들고, 컴포넌트나 서비스를 주입한 뒤 동작을 검증합니다. 최신 standalone 컴포넌트는 declarations가 아니라 imports에 넣어 테스트하는 점이 중요합니다.
테스트 파일은 보통 *.spec.ts 이름을 사용합니다.
1
2
src/app/counter/counter.component.ts
src/app/counter/counter.component.spec.ts
테스트에서 확인할 것은 “Angular가 잘 동작하는지”가 아니라 “내 코드의 사용자 관찰 결과가 맞는지”입니다. 내부 구현보다 버튼 클릭 후 화면이 바뀌는지, 서비스 호출 후 상태가 바뀌는지처럼 결과 중심으로 작성하면 리팩터링에 강해집니다.
2. standalone 컴포넌트 테스트
아래 컴포넌트는 signal로 카운트를 관리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p data-testid="count">현재 값: {{ count() }}</p>
<button type="button" (click)="increment()">증가</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
}
컴포넌트를 테스트할 때는 fixture.detectChanges()로 템플릿을 갱신하고, DOM에서 실제 표시 값을 확인합니다.
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
import { TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CounterComponent]
});
});
it('should create', () => {
const fixture = TestBed.createComponent(CounterComponent);
const component = fixture.componentInstance;
expect(component).toBeTruthy();
});
it('should increment count', () => {
const fixture = TestBed.createComponent(CounterComponent);
const component = fixture.componentInstance;
component.increment();
fixture.detectChanges();
expect(component.count()).toBe(1);
expect(fixture.nativeElement.textContent).toContain('현재 값: 1');
});
});
3. 사용자 이벤트 테스트
컴포넌트 메서드를 직접 부르는 테스트도 유용하지만, 버튼 클릭처럼 사용자가 실제로 하는 행동을 재현하면 더 믿을 만합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
it('should increment when user clicks button', () => {
const fixture = TestBed.createComponent(CounterComponent);
fixture.detectChanges();
const button: HTMLButtonElement =
fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
const countText = fixture.nativeElement.querySelector('[data-testid="count"]');
expect(countText.textContent).toContain('현재 값: 1');
});
data-testid는 테스트용 선택자를 안정적으로 만들 때 유용합니다. 스타일 클래스나 HTML 구조는 디자인 변경 때문에 자주 바뀔 수 있으므로, 테스트가 UI 리팩터링에 과하게 흔들리지 않도록 선택자를 신중하게 고르세요.
4. 서비스 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DataService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add data', () => {
service.addData('test');
expect(service.getData()).toContain('test');
});
});
서비스 테스트는 컴포넌트보다 빠르고 좁은 범위를 검증할 수 있습니다. 계산, 필터링, 상태 변경처럼 화면과 직접 연결되지 않는 로직은 서비스 또는 순수 함수로 분리하면 테스트가 쉬워집니다.
5. HTTP 서비스 테스트
API를 호출하는 서비스는 실제 서버에 요청하지 않고 테스트용 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
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
service = TestBed.inject(UserService);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => {
http.verify();
});
it('should load users', () => {
service.getUsers().subscribe(users => {
expect(users.length).toBe(2);
expect(users[0].name).toBe('Kim');
});
const request = http.expectOne('/api/users');
expect(request.request.method).toBe('GET');
request.flush([
{ id: 1, name: 'Kim' },
{ id: 2, name: 'Lee' }
]);
});
});
6. 테스트 실행
1
2
3
4
5
# 테스트 실행
ng test
# 단일 실행
ng test --watch=false
CI에서는 watch 모드를 끄고 한 번만 실행하는 방식이 일반적입니다. 프로젝트가 Vitest나 Jest 기반으로 구성되어 있다면 명령어는 달라질 수 있지만, 컴포넌트와 서비스의 검증 원칙은 같습니다.
7. 실습: Todo 필터 테스트하기
Day 20의 Todo 앱을 떠올리며 아래 테스트를 작성해보세요.
- 완료되지 않은 Todo만 반환하는
activeTodoscomputed를 만듭니다. - Todo 3개 중 1개를 완료 상태로 둡니다.
activeTodos()결과가 2개인지 검증합니다.- 완료 상태를 바꾼 뒤 결과가 다시 계산되는지 확인합니다.
이 실습은 signals가 테스트에서 어떻게 즉시 값을 갱신하는지 이해하는 데 좋습니다.
📝 정리
테스트 종류
- 유닛 테스트: 함수, 서비스, 작은 상태 로직 검증
- 컴포넌트 테스트: 템플릿과 사용자 이벤트 검증
- 통합 테스트: 여러 컴포넌트와 서비스의 연결 검증
- E2E 테스트: 실제 브라우저에서 주요 사용자 흐름 검증
체크리스트
- standalone 컴포넌트를
imports로 테스트할 수 있나요? - signal 값을 변경한 뒤 화면 갱신을 확인할 수 있나요?
- 버튼 클릭 같은 사용자 이벤트를 테스트할 수 있나요?
- 서비스 로직과 HTTP 요청을 분리해 검증할 수 있나요?
- CI에서 한 번만 실행되는 테스트 명령을 구성할 수 있나요?
📚 다음 학습
- 이전: Day 17: 성능 최적화
- 다음: Day 19: 배포하기
“테스트는 안정적인 앱의 기반입니다!” ✅
![[Angular 마스터하기] Day 18 - 테스팅 기초, 안전한 코드 만들기](/assets/img/posts/angular/angular-day-18.png)