next/passport 모듈을 살펴보다가 제일 먼저 테스트 코드를 봤는데 e2e 테스트 코드를 보았다.
크게 어렵지 않아 분석하기 편했고 무엇보다 많이 쓰는 오픈소스라 신뢰감이 있었다.
나는 함수 유닛테스트 정도 경험밖에 없었는데 좋은 경험이 됐다.
pactum 테스트 모듈을 사용한 이유
node 진영에서 많이 쓰는 jest, mocha등이 있지만 pactum은 처음 들어보았다.
pactum을 사용한 이유는 rest api 전용 test tool이다.
아래 jest와 간단하게 비교한 코드가 있는데 (찾아보면 더 많음) 확실히 pactum에서 지원하는 함수가 가독성이 더 좋다.
이런게 백엔드 e2e 테스트구나..
애플리 케이션의 처음과 끝을 테스트한다는 말이 나에게는 어려웠다. 코드를 보니 확실히 어떤 것을 말하는지 알게됐다.
실무에 적용하면?
e2e 테스트는 개발 공수가 너무 많이 들어 오히려 안하는 경우도 많이 있다고 알고있었다.
하지만 많이 바뀌지 않고 정말 중요한 부분의 서비스 로직에 e2e 테스트는 괜찮은 것 같다.
유지보수 측면에서도 코드를 이해하기 쉬울 것 같고 최소한의 가이드라인이 될 것 같다
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { spec, request } from 'pactum'; // rest api testing tool
import { AppModule as WithRegisterModule } from '../with-register/app.module';
import { AppModule as WithoutRegisterModule } from '../without-register/app.module';
// describe.each를 사용하여 WithRegisterModule, WithoutRegisterModule 모듈을 모두 테스트 한다.
describe.each`
AppModule | RegisterUse
${WithRegisterModule} | ${'with'}
${WithoutRegisterModule} | ${'without'}
`('Passport Module $RegisterUse register()', ({ AppModule }) => {
let app: INestApplication;
beforeAll(async () => {
const modRef = await Test.createTestingModule({
imports: [AppModule]
}).compile();
app = modRef.createNestApplication(); // 테스트 앱 생성
await app.listen(0); // 서버 실행
const url = (await app.getUrl()).replace('[::1]', 'localhost');
request.setBaseUrl(url); // http url 설정
});
describe('Authenticated flow', () => { // 인증 흐름
it('should be able to log in and get a jwt, then hit the secret route', async () => { // 인증이 필요한 경로에 대한 접근 권한이 되는지 확인하는 테스트 케이스
await spec() // spec은 팩텀 라이브러리에서 제공하는 http 요청 함수, http 요청 설정, 해당 요청에 대한 응답 검증
.post('/login')
.withBody({ username: 'test1', password: 'test' })
.expectStatus(201)
// return 값은 token
.stores('token', 'token'); // 토큰 값 저장
await spec()
.get('/private')
.withHeaders('Authorization', 'Bearer $S{token}')
.expectBody({ message: 'Hello secure world!' });
});
});
describe('UnauthenticatedFlow', () => { // 인증 되지 않았을 때 흐름
it('should return a 401 for an invalid login', async () => {
await spec()
.post('/login')
.withBody({ username: 'test1', password: 'not the right password' })
.expectStatus(401);
});
it('should return a 401 for an invalid JWT', async () => {
await spec()
.get('/private')
.withHeaders('Authorization', 'Bearer not-a-jwt')
.expectStatus(401);
});
});
afterAll(async () => {
await app.close();
});
});
// jest로 가능?
// it('/ (GET)', () => {
// return request(app.getHttpServer())
// .get('/')
// .set('Authorization', `Bearer ${token}`)
// .expect(200)
// .expect('Welcome to my Movie API');
// });
// jest로 가능하긴 하지만 전체적으로 작성하는 방법이 좀 더 어렵고, api 테스트에 특화 되어있다.
백엔드 e2e 테스트 코드를 보다 보니 프론트엔드에서는 어떤식으로 할까? 공식문서를 통해 확인해보고 싶었다.
얼마 전에 유튜브 임효성 개발자님의 오픈소스 까보기 영상에 swr 모듈을 보아서 나도 보았다.
나는 이전 프론트 엔드 개발을 할 때, cypress를 통해 e2e 테스트를 해보려고 했지만 느린 속도, 그리고 그때 당시에는 레퍼런스가 거의 없어 적응하다가 포기했던 경험이 있었다.
playwright는 ms에서 개발하였고 cypress보다 훨씬 빠른 속도라고 한다.
/* eslint-disable testing-library/prefer-screen-queries */
import { test, expect } from '@playwright/test'
// swr: 데이터를 불러오기 위한 React Hook 라이브러리
test.describe('Stream SSR', () => {
test('Basic SSR', async ({ page }) => {
// 에러가 발생하면 log에 추가
const log: any[] = []
await page.exposeFunction('consoleError', (msg: any) => log.push(msg))
// 페이지가 로드될 때 실행될 초기화 스크립트 등록
await page.addInitScript(`
const onError = window.onerror
window.onerror = (...args) => {
consoleError(...args)
onError(...args)
}
`)
await page.goto('./basic-ssr', { waitUntil: 'commit' }) // 페이지 로드
await expect(page.getByText('result:undefined')).toBeVisible() // 텍스트 보이는지 기대값
await expect(page.getByText('result:SSR Works')).toBeVisible()
await expect(page.getByText('history:[null,"SSR Works"]')).toBeVisible()
expect(log).toHaveLength(0) // log 배열에 아무것도 없으면 정상 동작
})
test('Partially Hydrate', async ({ page }) => {
const log: any[] = []
await page.exposeFunction('consoleError', (msg: any) => log.push(msg))
await page.addInitScript(`
const onError = window.onerror
window.onerror = (...args) => {
consoleError(...args)
onError(...args)
}
`)
await page.goto('./partially-hydrate', { waitUntil: 'commit' })
await expect(page.getByText('first data:undefined')).toBeVisible()
await expect(
page.getByText('second data (delayed hydration):undefined')
).toBeVisible()
await expect(page.getByText('first data:SSR Works')).toBeVisible()
await expect(
page.getByText('second data (delayed hydration):SSR Works')
).toBeVisible()
await expect(
page.getByText('first history:[null,"SSR Works"]')
).toBeVisible()
await expect(
page.getByText('second history:[null,"SSR Works"]')
).toBeVisible()
expect(log).toHaveLength(0)
})
})