├── .github ├── CODEOWNERS └── 한재원 │ ├── 1. 테스트 목적과 장애물.md │ ├── 2. 테스트 방법과 테스트 전략.md │ ├── 3. 처음 시작하는 단위 테스트.md │ └── 4. 목 객체.md ├── .gitignore ├── README.md ├── 강은비 ├── 01. 테스트 목적과 장애물 │ └── README.md ├── 02. 테스트 방법과 테스트 전략 │ └── README.md ├── 03. 처음 시작하는 단위 테스트 │ └── README.md ├── 04. 목 객체 │ └── README.md ├── 05. UI 컴포넌트 테스트 │ └── README.md ├── 08. UI 컴포넌트 탐색기 │ └── README.md └── 10. E2E 테스트 │ └── README.md ├── 고석영 ├── 01. 테스트 목적과 장애물 │ └── README.md ├── 02. 테스트 방법과 테스트 전략 │ ├── README.md │ ├── testing-dorito.jpeg │ ├── testing-ice-cream.png │ ├── testing-pyramid.png │ └── testing-trophy.jpeg ├── 03. 처음 시작하는 단위 테스트 │ └── README.md ├── 04. 웹 API 목 객체 기초 │ └── README.md ├── 05. UI 컴포넌트 테스트 │ ├── README.md │ └── image.png ├── 06. 커버리지 리포트 읽기 │ ├── README.md │ └── image.png ├── 07. 웹 애플리케이션 통합 테스트 │ └── README.md ├── 08. UI 컴포넌트 탐색기 │ ├── README.md │ ├── image01.png │ ├── image02.png │ └── image03.png ├── 09. 시각적 회귀 테스트 │ ├── README.md │ ├── chromatic-review.png │ ├── chromatic-setup-a.png │ ├── chromatic-setup-b.png │ ├── github-actions.png │ ├── github-chromatic.png │ ├── github_flow.png │ ├── reg-suit.png │ ├── screenshot.png │ ├── storycap-cli.png │ └── storycap-html.png ├── 10. E2E 테스트 │ ├── README.md │ ├── axe-playwright.png │ ├── debugging-e2e-brower.png │ ├── debugging-e2e-inspector.png │ └── seed-script.png └── README.md ├── 박상우 ├── 01. 테스트 목적과 장애물 │ └── README.md ├── 02.테스트 방법과 테스트 전략 │ └── README.md ├── 03. 처음 시작하는 단위 테스트 │ └── README.md ├── 04. 목 객체 │ └── README.md ├── 05. UI 컴포넌트 테스트 │ └── README.md ├── 06. 커버리지 리포트 읽기 │ └── README.md ├── 07. 웹 애플리케이션 통합 테스트 │ └── README.md ├── 08. UI 컴포넌트 탐색기 │ └── README.md ├── 09. 시각적 회귀 테스트 │ └── README.md └── 10. E2E 테스트 │ └── README.md ├── 박서영 ├── 01. 테스트 목적과 장애물 │ └── README.md ├── 02. 테스트 방법과 테스트 전략 │ └── README.md ├── 03. 처음 시작하는 단위 테스트 │ ├── README.md │ └── images │ │ ├── image_0.png │ │ ├── image_1.png │ │ └── image_2.png ├── 04. 목 객체 │ └── README.md ├── 05. UI 컴포넌트 테스트 │ └── README.md ├── 06. 커버리지 리포트 읽기 │ ├── README.MD │ └── images │ │ ├── image_0.png │ │ └── image_1.png ├── 07. 웹 애플리케이션 통합 테스트 │ └── README.MD ├── 08. UI 컴포넌트 탐색기 │ ├── README.MD │ └── images │ │ └── image_0.png ├── 09. 시각적 회귀 테스트 │ └── README.MD └── 10. E2E 테스트 │ └── README.MD ├── 이용훈 ├── Chapter 1 │ └── chapter1.md ├── Chapter 10 │ └── chapter10.md ├── Chapter 2 │ ├── chapter2.md │ └── image.png ├── Chapter 3 │ └── chapter3.md ├── Chapter 4 │ ├── chapter4.md │ ├── image.png │ ├── image1.png │ └── image2.png ├── Chapter 5 │ ├── chapter5.md │ ├── image.png │ ├── image1.png │ ├── image2.png │ ├── image3.png │ ├── image4.png │ └── image5.png ├── Chapter 6 │ └── chapter6.md ├── Chapter 7 │ └── chapter7.md └── Chapter 8 │ └── chapter8.md ├── 이우혁 ├── chapter1 │ └── README.md ├── chapter10 │ └── README.md ├── chapter2 │ └── README.md ├── chapter3 │ └── README.md ├── chapter4 │ └── README.md ├── chapter5 │ └── README.md ├── chapter6 │ └── README.md ├── chapter7 │ └── README.md ├── chapter8 │ └── README.md └── chapter9 │ └── README.md ├── 이재혁 ├── 01. 테스트 목적과 장해물 │ └── README.md ├── 02. 테스트 방법과 테스트 전략 │ └── README.md ├── 03. 처음 시작하는 단위 테스트 │ └── README.md ├── 04. 목 객체 │ └── README.md ├── 05. UI 컴포넌트 테스트 │ └── README.md ├── 06. 커버리지 리포트 읽기 │ └── README.md ├── 07. 웹 애플리케이션 통합 테스트 │ └── README.md ├── 08. UI 컴포넌트 탐색기 │ └── README.md └── 09. 시각적 회귀 테스트 │ └── README.md ├── 정소은 ├── Chapter01 │ └── README.md ├── Chapter02 │ └── README.md ├── Chapter03 │ ├── README.md │ └── image.png ├── Chapter04 │ └── README.md ├── Chapter05 │ └── README.md ├── Chapter06 │ ├── README.md │ ├── image-1.png │ └── image.png ├── Chapter07 │ └── README.md ├── Chapter08 │ └── README.md ├── Chapter09 │ └── README.md └── Chapter10 │ └── README.md ├── 정수지 ├── chapter01.md ├── chapter02.md ├── chapter03.md ├── chapter04.md ├── chapter05.md ├── chapter06.md ├── chapter07.md └── chapter08.md ├── 조명근 ├── 1주차 │ └── README.md ├── 2주차 │ └── README.md ├── 3주차 │ └── README.md ├── 4주차 │ └── READEME.md └── 6주차 │ └── READEME.md ├── 하현준 ├── 01. 테스트 목적과 장애물 │ └── README.md ├── 02. 테스트 방법과 테스트 전략 │ └── README.md ├── 03. 처음 시작하는 단위 테스트 │ └── README.md ├── 04. 목 객체 │ └── README.md ├── 05. UI 컴포넌트 테스트 │ └── README.md ├── 06. 커버리지 리포트 읽기 │ └── README.md ├── 07. 웹 애플리케이션 통합 테스트 │ └── README.md ├── 08. UI 컴포넌트 탐색기 │ └── README.md ├── 09. 시각적 회귀 테스트 │ └── README.md └── 10. E2E 테스트 │ └── README.md └── 황낙준 ├── 01 └── README.md ├── 02 └── README.md ├── 03 └── README.md ├── 04 └── README.md └── 05 ├── README.md ├── multi-select-chip-group.spec.ts └── phone-number-input.spec.ts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @haryan248 @dding-g @eunnbi @JoyJaewon @nakjun12 @ongddree @rarlala @samseburn @ssilver01 @wo-o29 @akatcn @jhlee0409 @SangWoo9734 2 | -------------------------------------------------------------------------------- /.github/한재원/1. 테스트 목적과 장애물.md: -------------------------------------------------------------------------------- 1 | # 1. 테스트 목적과 장애물 2 | 3 | ## 1.1 이 책의 구성 4 | 5 | - 테스트 방법 6 | 1. 함수 단위 테스트 7 | 2. UI 컴포넌트와 단위 테스트 8 | 3. UI 컴포넌트와 통합 테스트 9 | 4. UI 컴포넌트와 시각적 회귀 테스트 10 | 5. E2E 테스트 11 | - 테스트 프레임워크 및 도구 12 | - Jest: 명령줄 인터페이스 기반 테스트 프레임워크 및 테스트 러너 13 | - Playwright: 헤드리스 브라우저를 포함한 테스트 프레임워크 및 테스트 러너 14 | - reg-suit: 시각적 회귀 테스트 프레임워크 15 | - 스토리북: UI 컴포넌트 탐색기 16 | 17 | ## 1.2 테스트를 작성해야 하는 이유 18 | 19 | - 사업의 신뢰성 20 | - UI나 시스템 버그는 서비스의 이미지와 직결된다. 따라서 핵심 기능에 발생할 수 있는 버그는 사전에 발견할 수 있어야한다. 21 | - 높은 유지보수성 22 | - 코드 품질 향상 23 | - 원할한 협업 24 | - 테스트 코드는 글로 작성된 문서보다 우수한 사양서다. 구현할 코드가 어떤 기능을 제공하고, 어떤 방식으로 작동하는지 파악할수 있다. 25 | - 회귀 테스트 줄이기 26 | 27 | ## 1.3 테스트 작성의 장벽 28 | 29 | - 단기적인 관점에서 보면 테스트 작성은 많은 시간을 소모하지만 장기적인 관점에서 보면 오히려 시간이 절약된다. 특히 자동화된 테스트 코드는 반복작업에 걸리는 시간을 줄여주고, 팀에 큰 도움을 준다. 30 | -------------------------------------------------------------------------------- /.github/한재원/2. 테스트 방법과 테스트 전략.md: -------------------------------------------------------------------------------- 1 | # 2. 테스트 방법과 테스트 전략 2 | 3 | > 웹 어플리케이션은 여러 모듈을 조합해 만든다. 한가지 기능을 구현할때도 라이브러리 제공 함수, 로직 담당 함수, UI 과련 함수, API 서버, DB 서버 등 다양한 모듈을 활용하게된다. 이때 어디서부터 어디까지가 프론트엔드가 커버하는 테스트인지 주의해야한다. 4 | 5 | ## 테스트 범위와 목적 6 | 7 | - **프론트엔드 테스트 범위** 8 | 9 | 1. **정적 분석(Static Analysis)**: 타입스크립트를 활용. 타입추론. ESLint 10 | 2. **단위 테스트(Unit Test)**: 컴포넌트가 제대로 동작하는지, 특정 입력값을 받아 기대하는 출력값을 반환하는지 테스트. 엣지케이스도 테스트한다. 11 | 3. **통합 테스트(Integration Test)**: 여러 모듈을 연동한 기능을 테스트한다. 커다란 UI 컴포넌트의 인터랙션을 엣지케이스를 포함해서 확인한다. 12 | 4. **E2E 테스트(End to End Test)**: UI 테스트 뿐만 아니라 외부 스토리지와 같이 연동중인 하위 시스템을 포함해서 테스트한다. 입력 내용에 따라 저장된 값이 갱신되기 때문에 UI는 물론 연동된 외부 기능이 정상적으로 작동하는지 검증할 수 있다. 13 | 14 | - **프론트엔드 테스트 타입** 15 | 1. **기능 테스트 (인터렉션 테스트):** 개발된 기능에 문제가 없는지 검증하는 테스트 16 | 2. **비기능 테스트 (접근성 테스트):** accessibility 17 | 3. **회귀 테스트:** 특정 시점을 기준으로 전후 차이를 비교하여 문제가 있는지 검증하는 테스트 - storybook 18 | 19 | ## 테스트 전략 모델 20 | 21 | - 테스트 피라미드 모델 22 | - 수동 테스트 23 | - —E2E 테스트— 24 | - ——통합 테스트—— 25 | - ————단위 테스트———— 26 | → 하층부 테스트의 비중이 높을수록 더욱 안정적이고 가성비 높은 테스트가 가능하다. 27 | - 테스팅 트로피 모델 28 | - 통합 테스트의 비중이 가장 높음 29 | 30 | ## 테스트 전략 계획 31 | 32 | - 테스트가 없어 리팩터링이 불안한 경우 33 | - 먼저 릴리즈된 기능을 목록으로 테스트를 정리하고 리팩터링을 진행할때 목서버를 활용해 통합테스트를 함께 진행한다. 34 | - 반응형으로 제작된 프로젝트 35 | - 반응형처럼 디바이스간 서로 다른 스타일을 제공하는 경우, CSS가 적용된 렌더링 결과를 검증할 브라우저 테스트가 필요하다. 이런 상황에서 실시하는 테스트가 브라우저를 사용한 시각적 회귀 테스트이다. (스토리북) 36 | - 데이터베이스를 포함한 E2E 테스트가 필요한 경우 37 | - 목 서버가 아닌 실제 웹 API 서버를 사용해서 E2E테스트를 하고 싶아면 테스트용 스테이징 환경을 사용해야 한다. 38 | - 혹은 테스트할 시스템을 컨테이너화해 CI 환경에서 실행 한 후 연동 중인 여러 시스템과 함께 테스트할 수 있다. 39 | -------------------------------------------------------------------------------- /.github/한재원/3. 처음 시작하는 단위 테스트.md: -------------------------------------------------------------------------------- 1 | # 3. 처음 시작하는 단위 테스트 2 | 3 | ## 3.1 환경 설정 4 | 5 | - **테스트 프레임워크**: JavaScript/TypeScript에서 가장 인기 있는 **Jest** 사용. 6 | - **기능**: 간단한 설정, 목 객체(mock object) 지원, 코드 커버리지(code coverage) 제공. 7 | 8 | ## 3.2 테스트 구성 요소 9 | 10 | - **기본 구조**: 테스트 함수 `test()` 사용. 11 | - **예제 코드**: 12 | ```tsx 13 | test("1 + 2는 3이어야 한다", () => { 14 | expect(1 + 2).toBe(3); 15 | }); 16 | ``` 17 | - **설명**: 18 | - `test(설명, 함수)`: 테스트 케이스 정의. 19 | - `expect(value).matcher(expected)`: 예상 결과와 비교. 20 | 21 | ## 3.3 테스트 실행 방법 22 | 23 | ### 3.3.1 명령줄 인터페이스 실행 24 | 25 | - **기본 실행 방법**: 26 | ``` 27 | npm test 28 | ``` 29 | - **특정 테스트 실행**: 30 | ``` 31 | npm test -- "파일명" 32 | ``` 33 | 34 | ### 3.3.2 테스트 러너로 실행 35 | 36 | - Jest는 기본적으로 **테스트 러너** 역할을 하며, 여러 테스트 파일을 자동 실행함. 37 | 38 | ## 3.4 조건 분기 39 | 40 | - **조건이 있는 테스트**: 41 | 42 | ```tsx 43 | function add(a: number, b: number): number { 44 | return a + b; 45 | } 46 | 47 | test("양수 덧셈", () => { 48 | expect(add(1, 2)).toBe(3); 49 | }); 50 | ``` 51 | 52 | ## 3.5 에지 케이스와 예외 처리 53 | 54 | ### 3.5.1 TypeScript로 입력값 제한 및 예외 발생 55 | 56 | - **예외 테스트**: 57 | 58 | ```tsx 59 | function add(a: number, b: number): number { 60 | if (a < 0 || b < 0) { 61 | throw new Error("양수만 입력하세요"); 62 | } 63 | return a + b; 64 | } 65 | 66 | test("음수 입력 시 예외 발생", () => { 67 | expect(() => add(-1, 2)).toThrow("양수만 입력하세요"); 68 | }); 69 | ``` 70 | 71 | - `expect(() => 함수()).toThrow(메시지)`: 예외 발생 여부 확인. 72 | 73 | ## 3.6 용도별 매처 74 | 75 | ### 3.6.1 숫자 검증 76 | 77 | - `toBe`, `toEqual`, `toBeCloseTo` 사용. 78 | ```tsx 79 | expect(0.1 + 0.2).toBeCloseTo(0.3); 80 | ``` 81 | - `toBe(value)`: 원시값(숫자, 문자열 등)이 정확히 같은지 비교. 82 | - `toEqual(value)`: 객체나 배열의 구조적 동등성을 비교. 83 | - `toBeCloseTo(value)`: 부동소수점 연산의 오차를 고려한 비교. 84 | 85 | ### 3.6.2 문자열 검증 86 | 87 | - `toContain`, `toMatch` 사용. 88 | ```tsx 89 | expect("Hello World").toContain("World"); 90 | expect("Hello").toMatch(/^H/); 91 | ``` 92 | - `toContain(value)`: 특정 문자열이 포함되었는지 확인. 93 | - `toMatch(regex)`: 정규식을 이용한 패턴 매칭. 94 | - `toHaveLength(length)`: 문자열의 길이 검증. 95 | 96 | ### 3.6.3 배열 검증 97 | 98 | - `toContain`, `toHaveLength` 사용. 99 | ```tsx 100 | expect([1, 2, 3]).toContain(2); 101 | expect([1, 2, 3]).toHaveLength(3); 102 | ``` 103 | - `toContain(value)`: 배열에 특정 값이 포함되었는지 확인. 104 | - `toHaveLength(length)`: 배열의 길이 검증. 105 | 106 | ### 3.6.4 객체 검증 107 | 108 | - `toMatchObject`, `toHaveProperty` 사용. 109 | ```tsx 110 | const obj = { name: "Alice", age: 25 }; 111 | expect(obj).toMatchObject({ name: "Alice" }); 112 | expect(obj).toHaveProperty("age", 25); 113 | ``` 114 | - `toMatchObject(obj)`: 객체가 특정 부분과 일치하는지 검증. 115 | - `toHaveProperty(key, value)`: 특정 프로퍼티가 존재하는지 확인. 116 | 117 | ## 3.7 비동기 처리 테스트 118 | 119 | ### 3.7.1 테스트 함수 120 | 121 | - 비동기 함수의 결과 검증을 위해 **async/await** 또는 **Promise** 사용. 122 | 123 | ### 3.7.2 Promise 반환하는 함수 검증 124 | 125 | - `.resolves`, `.rejects` 사용. 126 | ```tsx 127 | test("비동기 함수가 성공해야 함", async () => { 128 | await expect(Promise.resolve(10)).resolves.toBe(10); 129 | }); 130 | ``` 131 | 132 | ### 3.7.3 Reject 검증 테스트 133 | 134 | - 실패하는 Promise 테스트. 135 | ```tsx 136 | test("비동기 함수가 실패해야 함", async () => { 137 | await expect(Promise.reject("오류")).rejects.toBe("오류"); 138 | }); 139 | ``` 140 | 141 | ### 3.7.4 테스트 결과가 제대로 일치하는지 확인하기 142 | 143 | - `expect.assertions()`을 사용하여 반드시 실행될 것을 보장. 144 | -------------------------------------------------------------------------------- /.github/한재원/4. 목 객체.md: -------------------------------------------------------------------------------- 1 | # 4. 목 객체 2 | 3 | ## 4.1 목 객체를 사용하는 이유 4 | 5 | - 테스트는 실제 환경과 유사할수록 재현성이 높지만, 실행 시간이 길어지거나 환경 구축이 어려워지는 문제가 있음. 6 | - 웹 API의 데이터를 다룰 때, API 응답이 네트워크 오류로 실패하는 경우를 포함해 성공과 실패를 모두 테스트해야 함. 7 | - 하지만 API 서버를 준비하지 않고도 API를 다루는 코드의 동작을 테스트할 수 있어야 함. 8 | - 이를 위해 **목 객체(Mock Object)**를 활용하면, API 없이도 효율적인 테스트가 가능함. 9 | 10 | ## 4.2 목 객체 용어 11 | 12 | - **테스트 더블(Test Double)**: 실제 객체를 대신하는 테스트용 객체. 13 | - **스텁(Stub)**: 특정 상황에서만 지정된 결과를 반환하는 목 객체. 14 | - **스파이(Spy)**: 함수 호출 여부 및 인자 확인을 위한 객체. 15 | - **모의(Mock)**: 테스트 도중 원하는 값을 반환하도록 설정된 객체. 16 | 17 | ## 4.3 웹 API 목 객체 기초 18 | 19 | - API를 직접 호출하는 대신, `fetch` 등의 웹 요청을 목 객체로 대체하여 네트워크 연결 없이 테스트 가능. 20 | - **코드 예제**: 21 | 22 | ```tsx 23 | import fetchProfile from "./profile"; 24 | 25 | export async function getProfile(): Promise { 26 | const profile = await fetchProfile(); 27 | return `Hello, ${profile}`; 28 | } 29 | ``` 30 | 31 | ```tsx 32 | import { getProfile } from "./index"; 33 | 34 | jest.mock("./profile", () => jest.fn(() => Promise.resolve("Jest"))); 35 | 36 | test("비동기 프로필 데이터 테스트", async () => { 37 | await expect(getProfile()).resolves.toBe("Hello, Jest"); 38 | }); 39 | ``` 40 | 41 | - `jest.mock()`을 사용하여 API 요청을 목 객체로 대체. 42 | 43 | ## 4.4 웹 API 목 객체 생성 함수 44 | 45 | - 웹 API를 목 객체로 대체하여 실제 API 서버 없이 데이터를 반환하도록 설정할 수 있음. 46 | - **코드 예제**: 47 | 48 | ```tsx 49 | export function fetchArticles(category: string): string[] { 50 | return category === "tech" ? ["Jest", "TypeScript"] : []; 51 | } 52 | ``` 53 | 54 | ```tsx 55 | import { fetchArticles } from "./index"; 56 | 57 | test("카테고리에 따른 기사 목록 반환", () => { 58 | expect(fetchArticles("tech")).toEqual(["Jest", "TypeScript"]); 59 | expect(fetchArticles("sports")).toEqual([]); 60 | }); 61 | ``` 62 | 63 | - `toEqual()`을 사용하여 배열 비교. 64 | 65 | ## 4.5 목 함수를 사용하는 스파이 66 | 67 | - 특정 함수가 호출되었는지, 몇 번 호출되었는지 등을 감시하는 기능. 68 | - **코드 예제**: 69 | 70 | ```tsx 71 | function logMessage(message: string): void { 72 | console.log(message); 73 | } 74 | 75 | test("콘솔 로그 함수 호출 여부 확인", () => { 76 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 77 | 78 | logMessage("Hello"); 79 | expect(spy).toHaveBeenCalledWith("Hello"); 80 | 81 | spy.mockRestore(); 82 | }); 83 | ``` 84 | 85 | - `jest.spyOn(객체, "메서드")`를 사용하여 특정 메서드를 감시할 수 있음. 86 | 87 | ## 4.6 웹 API 목 객체의 세부 사항 88 | 89 | - 네트워크 요청을 목 객체로 대체하여 테스트할 수 있음. 90 | - **코드 예제**: 91 | 92 | ```tsx 93 | global.fetch = jest.fn(() => 94 | Promise.resolve({ 95 | json: () => Promise.resolve({ name: "Jest" }), 96 | }) 97 | ); 98 | 99 | test("fetch 요청 모킹", async () => { 100 | const response = await fetch("https://api.example.com"); 101 | const data = await response.json(); 102 | expect(data).toEqual({ name: "Jest" }); 103 | }); 104 | ``` 105 | 106 | - `global.fetch`를 `jest.fn()`으로 대체하여 API 응답을 제어. 107 | 108 | ## 4.7 현재 시각에 의존하는 테스트 109 | 110 | - 현재 시각을 직접 다루면 테스트 실행 시마다 결과가 달라질 수 있음. 111 | - `jest.useFakeTimers()`를 이용해 특정 시간을 고정하여 테스트 가능. 112 | - **코드 예제**: 113 | 114 | ```tsx 115 | test("현재 시각 테스트", () => { 116 | jest.useFakeTimers().setSystemTime(new Date("2023-01-01")); 117 | 118 | const now = new Date().toISOString(); 119 | expect(now).toBe("2023-01-01T00:00:00.000Z"); 120 | 121 | jest.useRealTimers(); 122 | }); 123 | ``` 124 | 125 | - `jest.useFakeTimers().setSystemTime()`을 이용해 특정 날짜를 고정하여 테스트할 수 있음. 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # misc 5 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## image 2 | 3 | 4 | - ⏰ 스터디 일시: 5 | - 🏫 스터디 장소: 온라인 6 | - 📚 스터디 자료: 프런트엔드 개발을 위한 테스트 입문 7 | - 단위 테스트 예제 8 | - 원서: https://github.com/frontend-testing-book/unittest 9 | - 번역서: https://github.com/frontend-testing-book-kr/unittest 10 | - 실전 테스트 코드 예제 11 | - 원서: https://github.com/frontend-testing-book/nextjs 12 | - 번역서: https://github.com/frontend-testing-book-kr/nextjs 13 | 14 | --- 15 | 16 | ### 🚀 진행 방식 17 | 18 | - 스터디 시간에는 정리한 내용을 짧게 발표하고, 질문을 바탕으로 토론해요. 19 | 20 | ### 📝 내용 정리 21 | 22 | - 매주 공부한 내용을 바탕으로 마크다운으로 정리해요. 23 | - 모든걸 적으려 하기보단, 중요하다고 생각하는 내용 위주로 적어요. 24 | - 마크다운 작성법은 [여기](https://gist.github.com/ihoneymon/652be052a0727ad59601)를 참고해주세요. 25 | 26 | ### 🙋‍♂️ 질문 27 | 28 | - 질문은 많이 준비하면 할수록 좋아요. 29 | - WHAT HOW WHY 방식으로 질문을 준비해요. 30 | - 퀴즈 형식으로 만들어도 좋고, 같이 토론할만한 질문이어도 좋아요. 31 | - 스터디 시간에 질문들을 랜덤으로 뽑아서 답변하고, 해당 답변을 바탕으로 토론 후 정리해요. 32 | 33 | #### 질문 예시 34 | 35 | - 컴포넌트와 훅의 차이가 무엇인가요? (WHAT) 36 | - 리액트에서는 왜 순수성을 지키는게 중요한가요? (WHY) 37 | - 어떻게 하면 자식 컴포넌트의 상태를 부모 컴포넌트에서 관리할 수 있을까요? (HOW) 38 | 39 | ### 📌 스터디 방법 40 | 41 | 1. 레파지토리를 `clone`해주세요. 42 | 2. `main` 브랜치에서 [아이디] 브랜치를 만들어주세요. ex) `haryan248` 43 | 3. `[아이디]` 브랜치에서 본인의 주차 폴더에 정리한 내용을 push 해주세요. ex) `하현준/1주차/README.md` 44 | 4. `[아이디]` 브랜치에서 `main` 브랜치로 `PR(Pull Request)`를 보내주세요. ex) `docs: 000 챕터 00내용 학습` 45 | 5. `PR`은 마지막에 확인한 사람이 머지를 진행해주세요. 46 | 47 | ### 🏃‍♂️ 스터디원 48 | [스터디원 테이블 쉽게 만들기 >](https://dclcps.csb.app/) 49 | 50 | 51 | 58 | 65 | 72 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 94 | 101 | 108 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 130 | 137 | 144 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 166 | 167 | 168 | 169 | 170 |
52 | 57 | 59 | 64 | 66 | 71 | 73 | 78 |
하현준이재혁황낙준이용훈
88 | 93 | 95 | 100 | 102 | 107 | 109 | 114 |
정소은박상우고석영박서영
124 | 129 | 131 | 136 | 138 | 143 | 145 | 150 |
이우혁조명근강은비한재원
160 | 165 |
정수지
171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /강은비/01. 테스트 목적과 장애물/README.md: -------------------------------------------------------------------------------- 1 | # 테스트 목적과 장애물 2 | 3 | ## 테스트를 작성해야 하는 이유 4 | 5 | 1. 예상치 못한 버그를 조기에 발견해 서비스의 이미지를 지켜준다. 버그는 서비스의 이미지와 직결된다. (특히, 비즈니스 임팩트가 큰 핵심 기능) 6 | 2. 지속적인 리팩토링을 돕는다. 코드 수정 시 기존 기능이 깨지지 않는지 빠르게 확인할 수 있다. 7 | 3. 코드 품질 향상에 도움이 된다. 테스트를 함께 작성하면 지속적으로 구현 코드를 반추하게 된다. 8 | 9 | - 어떤 구현 코드의 테스트 작성이 어렵다는 것은 해당 코드가 너무 많은 역햘을 한다는 신호일 수 있다. 10 | - 웹 접근성을 높이는 데 기여한다. UI 컴포넌트 테스트 시 웹 접근성에서 유래한 API를 활용해 테스트 대상을 선택한다. 테스트 코드를 작성하면 자연스럽게 보조기기를 이용하는 사람에게도 콘텐츠가 인식될 수 있는지 확인할 수 있다. 11 | 12 | 4. 원활한 리뷰가 이루어진다. 테스트는 사양서 역할을 해 구현 코드의 작동 방식을 파악하고 실제 사양서대로 구현되었는지 확인할 수 있어 리뷰어의 부담이 줄어든다. 13 | 5. 테스트 자동화는 회귀 테스트를 줄이는 최적의 방법이다. 14 | 15 | > **테스트 대상 선택 API가 웹 접근성에서 유래한 이유** 16 | > 17 | > 기존 XPath와 CSS 선택자의 문제점 18 | > 19 | > - XPath: DOM 트리 구조가 조금만 변해도 테스트가 쉽게 깨짐. 20 | > - CSS 선택자: CSS Module, styled-components, Tailwind 등 클래스네임이 동적으로 생성되면 선택자 기반 테스트가 불안정함. 21 | > 22 | > 접근성 기반 선택자 23 | > 24 | > - ARIA 표준은 특정 프레임워크나 CSS, DOM 트리 구조 등에 의존하지 않기 때문에 안정적인 테스트 작성이 가능하다. 25 | > - 스크린 리더 친화적인 코드는 UI 요소의 역할(role)과 이름(accessible name)이 명확하게 정의되어 있어, 테스트 도구도 이를 쉽게 인식하고 검사할 수 있다. 26 | > 27 | > 참고: https://tech.wonderwall.kr/articles/a11ydriventestautomation/ 28 | 29 | ## 테스트 작성의 장벽 30 | 31 | - 테스트 작성 시간 부족, 팀원들의 기술력 부족 32 | - 단기적인 관점: 테스트 작성은 많은 시간을 소모한다. 33 | - 장기적인 관점: 버그를 빠르게 발견할 수 있고, QA 과정에서 반복되는 버그 수정 작업이 줄어든다. 34 | | | 테스트 자동화 없는 경우 | 테스트 자동화 있는 경우 | 35 | | -- | -- | -- | 36 | | 개인 | 기능 개발 | 기능 개발 + 테스트 자동화 | 37 | | 팀 | 기능 개발 + QA 반복 작업 | 기능 개발 + 테스트 자동화 | 38 | | 팀 (장기) |기능 개발 + QA 반복 작업 + 회귀 테스트 | 기능 개발 + 테스트 자동화 | 39 | 40 | > **Action Items** 41 | > 42 | > - 테스트 코드 작성 능력 기르기 (책 완독!!) 43 | > - 팀에게 가장 필요한 테스트부터 작성해보기 (일찍, 조금씩, 자주) 44 | 45 | ## Questions 46 | 47 | - 단기적인 개발 속도와 장기적인 코드 안정성을 어떻게 균형 있게 유지해야 할까? (개발 속도를 유지하면서도 테스트를 지속적으로 도입하는 전략) 48 | - GPT와 Copilot과 같은 AI를 활용하면 테스트 작성 시간을 줄일 수 있을까? 49 | -------------------------------------------------------------------------------- /강은비/02. 테스트 방법과 테스트 전략/README.md: -------------------------------------------------------------------------------- 1 | # 테스트 방법과 테스트 전략 2 | 3 | ## 테스트 범위 4 | 5 | - **정적분석**: 타입스크립트나 ESLint가 제공하는 기능 활용 6 | - **단위 테스트**: 모듈 하나에 대한 테스트, 코너 케이스를 이용해 함수가 고려하지 못한 부분이 없는지 검증하는데 유용하다. 7 | - **통합 테스트**: 여러 모듈 조합에 대한 테스트, 테스트 목적에 따라 범위가 좁히거나 넓힐 수도 있다. 외부 API를 사용한다면 목 서버를 활용해 구현 코드를 수정하지 않고 테스트할 수 있다. 8 | - **E2E 테스트**: 프로덕션과 최대한 동일한 환경에서 실제 백엔드 API로 사용자 동작과 흐름의 전과정을 검증하는 테스트, 테스트용 스테이징 환경을 만들거나 테스트할 시스템을 컨테이너화할 수 있다. 9 | 10 | ## 테스트 목적 11 | 12 | - **기능 테스트**: 웹 프론트엔드의 기능 테스트는 대부분 인터랙션 테스트이다. 13 | - **비기능 테스트**: 접근성 테스트는 비기능 테스트의 한 종류이다. 14 | - **시각적 회귀 테스트**: 헤드리스 브라우저에 그려진 내용을 캡쳐해 이전에 캡쳐한 이미지 간 차이를 검증한다. 반응형 웹에서 렌더링 결과를 검증할 때 유용하다. 15 | 16 | ## 테스트 전략 모델 17 | 18 | - **아이스크림 콘**: 안티패턴, 외부 모듈의 의존성 때문에 아주 가끔씩 실패하는 불안정한 테스트가 비교적 많다. (긴 실행시간, 낮은 안정성, 높은 비용, 높은 유사성) 19 | - **테스트 피라미드**: 하층부 테스트의 비중이 높아 안정적이고 가성비 높은 테스트가 가능하다. (짧은 실행시간, 높은 안정성, 낮은 비용, 낮은 유사성) 20 | - **테스팅 트로피**: 정적 분석을 포함하고, 통합 테스트 비중이 가장 높다. 실행 속도가 빠르면서 실제 제품과 유사한 테스트가 가능하다. 21 | 22 | > **주의해야 할 점** 23 | > 24 | > - 프로젝트 전체적인 관점에서 테스트 범위가 중복되는지 검토해보자. 25 | > - 어떤 테스트 전략이 프로젝트에 적합한지 항상 되돌아보자. 26 | -------------------------------------------------------------------------------- /강은비/03. 처음 시작하는 단위 테스트/README.md: -------------------------------------------------------------------------------- 1 | # 처음 시작하는 단위 테스트 2 | 3 | ## 테스트 구성 요소 4 | 5 | - 테스트명: 테스트 코드가 어떤 의도로 작성되었으며, 어떤 작업이 포함됐는지 명확하게 표현해야 한다. 6 | - 테스트 함수: 단언문을 포함한다. 단언문은 검증값과 기대값이 일치하는지 검증한다. 7 | - Jest Matcher: https://jestjs.io/docs/using-matchers 8 | - `expect` API Doc: https://jestjs.io/docs/expect 9 | - 테스트 그룹: 연관성 있는 테스트들의 그룹, 중첩이 가능하다. 10 | 11 | ## 단위 테스트 예시 12 | 13 | ### 조건 분기 14 | 15 | ```ts 16 | export const add = (a: number, b: number) => { 17 | const sum = a + b; 18 | if (sum > 100) return 100; 19 | return sum; 20 | }; 21 | ``` 22 | 23 | ```ts 24 | // Bad Example 25 | test("50 + 50은 100", () => { 26 | expect(add(50, 50)).toBe(100); 27 | }); 28 | 29 | // 구현 코드를 모른다면 `70+80`이 `100`이 된다는 것을 이해할 수 없다. 30 | test("70 + 50은 100", () => { 31 | expect(add(70, 50)).toBe(100); 32 | }); 33 | ``` 34 | 35 | ```ts 36 | // Good Example 37 | 38 | // 함수의 명세와 정책이 명확하게 드러난다. 39 | // 설명이 잘 작성된 테코는 그 자체로 명세 역할을 수행한다. 40 | test("반환값은 첫 번째 매개변수와 두 번째 매개변수를 더한 값이다", () => { 41 | expect(add(50, 50)).toBe(100); 42 | }); 43 | 44 | test("반환값의 상한은 '100'이다", () => { 45 | expect(add(70, 80)).toBe(100); 46 | }); 47 | ``` 48 | 49 | ### 예외 발생 50 | 51 | - `toThrow` 매처를 사용해 예외 발생을 검증할 수 있다. 52 | - 인수를 할당하면 예외의 세부 사항을 검증할 수 있다. 53 | - 정규식: 에러 메시지가 정규식 패턴과 일치하는지 검증 54 | - 문자열: 에러 메시지가 문자열을 포함하는지 검증 55 | - 에러 객체: 에러 메시지가 객체의 `message` 속성값과 일치하는지 검증 56 | - 에러 클래스: 에러 객체가 클래스의 인스턴스인지 검증 57 | 58 | > 참고: https://jestjs.io/docs/expect#tothrowerror 59 | 60 | ### 비동기 처리 61 | 62 | 비동기 처리가 포함된 함수를 검증하는데 다양한 방법이 있다. 63 | 64 | **`then/catch` 메서드에 전달한 함수에 단언문 작성** 65 | 66 | ```ts 67 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", () => { 68 | return wait(50).then((duration) => { 69 | expect(duration).toBe(50); 70 | }); 71 | }); 72 | 73 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", () => { 74 | return timeout(50).catch((duration) => { 75 | expect(duration).toBe(50); 76 | }); 77 | }); 78 | ``` 79 | 80 | **`resolves/rejects` 매처 사용** 81 | 82 | ```ts 83 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", () => { 84 | // 비동기 처리를 테스트할 때 테스트 함수가 동기 함수라면 반드시 단언문을 반환해야 한다. 85 | return expect(wait(50)).resolves.toBe(50); 86 | }); 87 | 88 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", () => { 89 | // make sure to add a return statement 90 | return expect(timeout(50)).rejects.toBe(50); 91 | }); 92 | ``` 93 | 94 | > 참고 95 | > 96 | > - https://jestjs.io/docs/expect#resolves 97 | > - https://jestjs.io/docs/expect#rejects 98 | 99 | **`async/await` 사용** 100 | 101 | ```ts 102 | // resolves/rejects 매처 사용하는 단언문에 await 키워드 붙이기 103 | 104 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", async () => { 105 | await expect(wait(50)).resolves.toBe(50); 106 | }); 107 | 108 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", async () => { 109 | await expect(timeout(50)).rejects.toBe(50); 110 | }); 111 | ``` 112 | 113 | ```ts 114 | // Promise를 반환하는 함수에 await 키워드 붙이기 115 | 116 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", async () => { 117 | expect(await wait(50)).toBe(50); 118 | }); 119 | 120 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", async () => { 121 | expect.assertions(1); // 테스트 중 실행될 단언문의 개수 검증 122 | try { 123 | await timeout(50); 124 | } catch (err) { 125 | expect(err).toBe(50); // 반드시 실행되어야 함 126 | } 127 | }); 128 | ``` 129 | 130 | > 참고: https://jestjs.io/docs/expect#expectassertionsnumber 131 | 132 | ## Key Takeaway 133 | 134 | - 테스트 대상의 명세와 정책이 명확하게 드러나도록 테스트 설명을 작성하자. 구현 내용을 모르더라도 테스트 설명을 보고 동작 방식을 이해할 수 있게! 135 | - Matcher들이 정말 많은데 그때그때 필요한 걸 공식문서에서 찾아서 사용해야 할 것 같다. 136 | -------------------------------------------------------------------------------- /강은비/04. 목 객체/README.md: -------------------------------------------------------------------------------- 1 | # 목 객체 2 | 3 | ## 목 객체를 사용하는 이유 4 | 5 | - 태스트 대상이 외부 시스템이나 객체에 의존하지 않고 독립적으로 테스트될 수 있다. 6 | - 예상치 못한 외부 요인으로부터 테스트를 보호하고, 테스트의 예측 가능성을 높일 수 있다. 7 | - 테스트 실행 속도를 향상시키고, 테스트 환경의 구성을 단순화하는데 도움이 된다. 8 | - 상황에 따라 `stub`(대역)이나 `spy`(기록용)를 사용한다. 9 | 10 | > 참고: https://jestjs.io/docs/jest-object 11 | 12 | ## 목 모듈을 활용한 스텁 13 | 14 | 모듈 전체 혹은 일부를 스텁으로 대체할 수 있다. 15 | 16 | ### 모듈 전체를 스텁으로 대체하기 17 | 18 | ```ts 19 | // greet 모듈 전체를 스텁으로 대체하기 20 | 21 | import { greet, sayGoodBye } from "./greet"; 22 | 23 | jest.mock("./greet", () => ({ 24 | // greet 함수가 존재하지 않음 25 | sayGoodBye: (name: string) => `Good bye, ${name}.`, // sayGoodBye 함수 대체 26 | })); 27 | 28 | test("인사말이 구현되어 있지 않다(원래 구현과 다르게)", () => { 29 | expect(greet).toBe(undefined); 30 | }); 31 | 32 | test("작별 인사를 반환한다(원래 구현과 다르게)", () => { 33 | const message = `${sayGoodBye("Taro")} See you.`; 34 | expect(message).toBe("Good bye, Taro. See you."); 35 | }); 36 | ``` 37 | 38 | ### 모듈 일부를 스텁으로 대체하기 39 | 40 | ```ts 41 | import { greet, sayGoodBye } from "./greet"; 42 | 43 | jest.mock("./greet", () => ({ 44 | ...jest.requireActual("./greet"), // 실제 모듈의 구현을 가져옴 45 | sayGoodBye: (name: string) => `Good bye, ${name}.`, // sayGoodBye 함수만 대체됨 46 | })); 47 | 48 | test("인사말을 반환한다(원래 구현대로)", () => { 49 | expect(greet("Taro")).toBe("Hello! Taro."); 50 | }); 51 | 52 | test("작별 인사를 반환한다(원래 구현과 다르게)", () => { 53 | const message = `${sayGoodBye("Taro")} See you.`; 54 | expect(message).toBe("Good bye, Taro. See you."); 55 | }); 56 | ``` 57 | 58 | > **Key Point**: 실무에서는 라이브러리를 대체할 때 목 모듈을 가장 많이 사용한다. 59 | > 60 | > ```ts 61 | > jest.mock("next/router", () => require("next-router-mock")); 62 | > ``` 63 | 64 | ## 목 함수를 사용하는 스파이 65 | 66 | - Jest에서 "목 함수 = 스파이"이다. 67 | - 스파이는 태스트 대상에 발생한 입출력을 기록하는 객체이다. (인수, 호출 횟수 기록) 68 | - 스파이에 기록된 값을 검증하면 의도한 대로 기능이 작동하는지 확인할 수 있다. 69 | 70 | ### 실행 여부 검증 71 | 72 | ```ts 73 | test("목 함수가 실행됐다", () => { 74 | const mockFn = jest.fn(); // 목 함수 생성 75 | mockFn(); 76 | expect(mockFn).toBeCalled(); 77 | }); 78 | 79 | test("목 함수가 실행되지 않았다", () => { 80 | const mockFn = jest.fn(); 81 | expect(mockFn).not.toBeCalled(); 82 | }); 83 | ``` 84 | 85 | ### 실행 횟수 검증 86 | 87 | ```ts 88 | test("목 함수는 실행 횟수를 기록한다", () => { 89 | const mockFn = jest.fn(); 90 | mockFn(); 91 | expect(mockFn).toHaveBeenCalledTimes(1); 92 | mockFn(); 93 | expect(mockFn).toHaveBeenCalledTimes(2); 94 | }); 95 | ``` 96 | 97 | ### 실행 시 인수 검증 98 | 99 | ```ts 100 | test("목 함수는 실행 시 인수를 기록한다", () => { 101 | const mockFn = jest.fn(); 102 | function greet(message: string) { 103 | mockFn(message); // 인수를 받아 실행된다. 104 | } 105 | greet("hello"); // "hello"를 인수로 실행된 것이 mockFn에 기록된다. 106 | expect(mockFn).toHaveBeenCalledWith("hello"); 107 | }); 108 | 109 | test("목 함수는 실행 시 인수가 객체일 때에도 검증할 수 있다", () => { 110 | const mockFn = jest.fn(); 111 | checkConfig(mockFn); 112 | expect(mockFn).toHaveBeenCalledWith({ 113 | mock: true, 114 | feature: { spy: true }, 115 | }); 116 | }); 117 | 118 | test("expect.objectContaining를 사용한 부분 검증", () => { 119 | const mockFn = jest.fn(); 120 | checkConfig(mockFn); 121 | expect(mockFn).toHaveBeenCalledWith( 122 | expect.objectContaining({ 123 | feature: { spy: true }, 124 | }) 125 | ); 126 | }); 127 | ``` 128 | 129 | > **Key Point**: 목 함수를 사용하는 스파이는 테스트 대상의 인수에 함수가 있을 때 유용하게 활용할 수 있다. 130 | > 131 | > ```ts 132 | > test("목 함수를 테스트 대상의 인수로 사용할 수 있다", () => { 133 | > const mockFn = jest.fn(); 134 | > greet("Jiro", mockFn); 135 | > expect(mockFn).toHaveBeenCalledWith("Hello! Jiro"); 136 | > }); 137 | > ``` 138 | 139 | ## 웹 API 목 객체 140 | 141 | 웹 API를 재현할 때 다음과 같은 메서드들을 주로 사용한다. 142 | 143 | - `jest.spyOn(object, methodName)`: 목 함수를 생성하고, `object[methodName]` 호출을 추적한다. 반환값은 [Jest Mock Function](https://jestjs.io/docs/mock-function-api)이다. 144 | - `mockFn.mockResolvedValueOnce(value)`: 목 함수에 `value`를 resolve하는 Promise를 반환하는 구현체를 주입한다. 145 | - `mockFn.mockRejectedValueOnce(value)`: 목 함수에 `value`와 함께 reject하는 Promise를 반환하는 구현체를 주입힌다. 146 | 147 | **예시** 148 | 149 | ```ts 150 | jest.mock("../fetchers"); 151 | 152 | function mockPostMyArticle(input: ArticleInput, status = 200) { 153 | if (status > 299) { 154 | return jest 155 | .spyOn(Fetchers, "postMyArticle") 156 | .mockRejectedValueOnce(httpError); 157 | } 158 | try { 159 | // 입력값 검증 후 응답 데이터를 교체한다. 160 | checkLength(input.title); 161 | checkLength(input.body); 162 | return jest.spyOn(Fetchers, "postMyArticle").mockResolvedValue(input); 163 | } catch (err) { 164 | return jest 165 | .spyOn(Fetchers, "postMyArticle") 166 | .mockRejectedValueOnce(httpError); 167 | } 168 | } 169 | 170 | function inputFactory(input?: Partial) { 171 | return { 172 | tags: ["testing"], 173 | title: "타입스크립트를 사용한 테스트 작성법", 174 | body: "테스트 작성 시 타입스크립트를 사용하면 테스트의 유지 보수가 쉬워진다", 175 | ...input, 176 | }; 177 | } 178 | 179 | test("유효성 검사에 성공하면 성공 응답을 반환한다", async () => { 180 | // 유효성 검사에 통과하는 입력을 준비한다. 181 | const input = inputFactory(); 182 | // 입력값을 포함한 성공 응답을 반환하는 목 객체를 만든다. 183 | const mock = mockPostMyArticle(input); 184 | // input을 인수로 테스트할 함수를 실행한다. 185 | const data = await postMyArticle(input); 186 | // 취득한 데이터에 입력 내용이 포함됐는지 검증한다. 187 | expect(data).toMatchObject(expect.objectContaining(input)); 188 | // 목 함수가 호출됐는지 검증한다. 189 | expect(mock).toHaveBeenCalled(); 190 | }); 191 | 192 | test("유효성 검사에 실패하면 reject된다", async () => { 193 | expect.assertions(2); 194 | // 유효성 검사에 통과하지 못하는 입력을 준비한다. 195 | const input = inputFactory({ title: "", body: "" }); 196 | // 입력값을 포함한 성공 응답을 반환하는 목 객체를 만든다. 197 | const mock = mockPostMyArticle(input); 198 | // 유효성 검사에 통과하지 못하고 reject됐는지 검증한다. 199 | await postMyArticle(input).catch((err) => { 200 | // 에러 객체가 reject됐는지 검증한다. 201 | expect(err).toMatchObject({ err: { message: expect.anything() } }); 202 | // 목 함수가 호출됐는지 검증한다. 203 | expect(mock).toHaveBeenCalled(); 204 | }); 205 | }); 206 | 207 | test("데이터 취득에 실패하면 reject된다", async () => { 208 | expect.assertions(2); 209 | // 유효성 검사에 통과하는 입력값을 준비한다. 210 | const input = inputFactory(); 211 | // 실패 응답을 반환하는 목 객체를 만든다. 212 | const mock = mockPostMyArticle(input, 500); 213 | // reject됐는지 검증한다. 214 | await postMyArticle(input).catch((err) => { 215 | // 에러 객체가 reject됐는지 검증한다. 216 | expect(err).toMatchObject({ err: { message: expect.anything() } }); 217 | // 목 함수가 호출됐는지 검증한다. 218 | expect(mock).toHaveBeenCalled(); 219 | }); 220 | }); 221 | ``` 222 | 223 | > 만약 팀에서 Mock server를 사용하면 있다면 일일이 API 호출 함수를 모킹하는 것보다 목 서버를 사용하는 게 효율적일 것 같다. 224 | 225 | ## 현재 시각에 의존하는 테스트 226 | 227 | - 테스트가 현재 시각에 의존하면 테스트를 언제 실행하냐에 따라 테스트가 실패 혹은 성공할 수 있어 일관성을 보장하기 어렵다. 228 | - 테스트 실행 환경의 현재 시각을 고정하면 언제 실행되더라도 동일한 테스트 결과를 얻을 수 있다. 229 | - `jest.useFakeTimers`: 가짜 타이머 사용 230 | - `jest.setSystemTime`: 가짜 타이머에서 사용할 현재 시각 설정 231 | - `jest.useRealTimers`: 실제 타이머 사용하도록 지시 232 | 233 | ```ts 234 | describe("greetByTime 함수", () => { 235 | // 테스트 실행하기 전에 수행해야 할 공통 설정 작업 지정 236 | beforeEach(() => { 237 | jest.useFakeTimers(); 238 | }); 239 | 240 | // 각 테스트 종료 후에 수행해야 할 공통 파기 작업 지정 241 | afterEach(() => { 242 | jest.useRealTimers(); 243 | }); 244 | 245 | test("아침에는 '좋은 아침입니다'를 반환한다", () => { 246 | jest.setSystemTime(new Date(2023, 4, 23, 8, 0, 0)); 247 | expect(greetByTime()).toBe("좋은 아침입니다"); 248 | }); 249 | 250 | test("점심에는 '식사는 하셨나요'를 반환한다", () => { 251 | jest.setSystemTime(new Date(2023, 4, 23, 14, 0, 0)); 252 | expect(greetByTime()).toBe("식사는 하셨나요"); 253 | }); 254 | 255 | test("저녁에는 '좋은 밤 되세요'를 반환한다", () => { 256 | jest.setSystemTime(new Date(2023, 4, 23, 21, 0, 0)); 257 | expect(greetByTime()).toBe("좋은 밤 되세요"); 258 | }); 259 | }); 260 | ``` 261 | -------------------------------------------------------------------------------- /강은비/05. UI 컴포넌트 테스트/README.md: -------------------------------------------------------------------------------- 1 | # UI 컴포넌트 테스트 2 | 3 | ## UI 컴포넌트 기능 4 | 5 | - 데이터 렌더링 6 | - 사용자 입력 전달 7 | - 웹 API 연동 8 | - 데이터 변경 9 | 10 | ## UI 컴포넌트 테스트 시작하기 11 | 12 | ### 요소 취득하기 13 | 14 | - [Testing Library](https://testing-library.com/docs/queries/about/#priority) 공식문서에서 **접근성** 기준으로 요소를 찾는 방법의 우선순위를 지정했다. 15 | - `getByRole`: 접근성 트리에 노출되는 요소를 찾을 때 사용된다. 명시적으로 `role` 속성이 할당된 요소뿐만 아니라 암묵적 역할을 가진 요소도 취득할 수 있다. `name` 옵션을 사용해 "접근 가능한 이름"으로 필터링할 수 있다. 16 | - 참고: [Role list](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles), [Accessible name](https://developer.mozilla.org/en-US/docs/Glossary/Accessible_name) 17 | - 예시: `getByRole("button", { name: /submit/i });` 18 | - `getByLabelText`: 폼 필드를 찾을 때 사용하기 좋다. 19 | - `getByPlaceholder`: 인풋 찾을 때 사용하기 좋다. 20 | - `getByText`: 비 상호작용적인 요소를 (div, span) 찾을 때 사용된다. 21 | - `getByDisplayValue`: 폼 요소의 현재 값 기반으로 요소를 찾을 때 유용하다. 22 | - `getByAltText`: 대체 텍스트를 지원하는 요소를 찾을 때 유용하다. 23 | - `getByTitle` 24 | - `getByTestId` 25 | - React Testing Library에서 제공하는 `logRoles` 함수를 사용해 렌더링 결과물에서 역할과 접근가능한 이름을 확인할 수 있다. 26 | 27 | > 요소 취득할 때 명시적/암묵적 역할과 접근 가능한 이름을 적극적으로 활용하자! 자연스럽게 웹 접근성이 개선될 것이다. 28 | 29 | | Type of Query | 0 Matches | 1 Match | >1 Matches | Retry (Async/Await) | 30 | | ------------- | ----------- | -------------- | ------------ | ------------------- | 31 | | getBy... | Throw error | Return element | Throw error | No | 32 | | getAllBy... | Throw error | Return array | Return array | No | 33 | | queryBy... | Return null | Return element | Throw error | No | 34 | | queryAllBy... | Return [] | Return array | Return array | No | 35 | | findBy... | Throw error | Return element | Throw error | Yes | 36 | | findAllBy... | Throw error | Return array | Return array | Yes | 37 | 38 | > **React Testing Library에서의 올바른 쿼리 사용법** 39 | > 40 | > 참고: [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) 41 | > 42 | > 1. 요소가 없음을 증명할 때만 `query*` 함수를 사용해라 43 | > 44 | > ```ts 45 | > // ❌ 46 | > expect(screen.queryByRole("alert")).toBeInTheDocument(); 47 | > 48 | > // ✅ 49 | > expect(screen.getByRole("alert")).toBeInTheDocument(); 50 | > expect(screen.queryByRole("alert")).not.toBeInTheDocument(); 51 | > ``` 52 | > 53 | > 2. 즉시 이용할 수 없는 요소를 찾을 때 `waitFor` 대신 `find*` 함수를 사용해라 54 | > 55 | > ```ts 56 | > // ❌ 57 | > const submitButton = await waitFor(() => 58 | > screen.getByRole("button", { name: /submit/i }) 59 | > ); 60 | > 61 | > // ✅ 62 | > const submitButton = await screen.findByRole("button", { name: /submit/i }); 63 | > ``` 64 | 65 | ## 상호작용 테스트 66 | 67 | - `@testing-library/user-event` 라이브러리를 사용하는 것을 권장한다. 68 | - `@testing-library/react` 에서 제공하는 `fireEvent` 보다 사용자 상호작용을 실제와 더 가깝게 재현할 수 있다. 69 | - 참고: [공식문서 - Differences from fireEvent](https://testing-library.com/docs/user-event/intro/#differences-from-fireevent) 70 | 71 | ```ts 72 | // Click 이벤트만 발생 73 | fireEvent.click(screen.getByRole("button", { name: "테스트" })); 74 | 75 | // Click 이벤트 + Mouseover 이벤트와 같이 발생가능한 모든 이벤트 발생 76 | userEvent.click(screen.getByRole("button", { name: "테스트" })); 77 | ``` 78 | 79 | ## 커스텀 매처 사용하기 80 | 81 | - `@testing-library/jest-dom` 라이브러리에서 추가적인 매처를 제공한다. 82 | - DOM 상태를 쉽게 검증할 수 있는 커스텀 매처를 사용하자 83 | - 예시: `toBeDisabled`, `toBeEnabled`, `toBeInTheDocument`, `toHaveTextContent`, `toBeChecked` 등 84 | 85 | ```ts 86 | const button = screen.getByRole("button", { name: /disabled button/i }); 87 | 88 | // ❌ 89 | expect(button.disabled).toBe(true); 90 | // error message: 91 | // expect(received).toBe(expected) // Object.is equality 92 | // 93 | // Expected: true 94 | // Received: false 95 | 96 | // ✅ 97 | expect(button).toBeDisabled(); 98 | // error message: 99 | // Received element is not disabled: 100 | // ; 40 | Primary.parameters = { 41 | backgrounds: { default: "blue" }, // 이 스토리에서만 적용 42 | }; 43 | ``` 44 | 45 | 46 | → 이러한 방식을 통해 **기본 설정을 유지한채 각 스토리에서 필요한 설정만 변경할 수 있다** 47 | 48 | - UI 컴포넌트는 props에 전달된 값에 따라 다른 스타일과 기능을 제공한다. 스토리북 탐색기에서는 props를 변경해 컴포넌트가 어떻게 렌더링되는지 실시간으로 디버깅할 수 있다. 이를 Controls라 한다. `@storybook/addon-controls`라는 애드온에서 제공한다 49 | - UI 컴포넌트는 props로 이벤트를 전달받아 이를 호출하기도 한다. 이벤트 핸들러가 어떻게 호출됐는지 로그를 출력하는 기능이 Actions이며, `@storybook/addon-actions` 패키지에서 제공한다 50 | - 반응형으로 구현한 UI 컴포넌트는 화면 크기별로 스토리를 등록할 수 있다. `@storybook/addon-viewport` 패키지에서 지원한다 51 | - Context API에 의존하는 스토리에는 스토리북의 데코레이터를 활용하는 것이 편리하다. 또한 초기값을 주입할 수 있도록 Provider를 만들면 Context의 상태에 의존하는 UI를 간단하게 재현할 수 있다 52 | - 데코레이터는 각 스토리의 렌더링 함수에 적용할 Wrapper이다. 예를 들어 UI 컴포넌트 외곽에 padding을 주고 싶다면 다음과 같이 데코레이터 함수를 `decorators` 배열에 추가한다: 53 | 54 | ```jsx 55 | export default { 56 | title: "ChildCompoent", 57 | component: ChildCompoent, 58 | decorators: [ 59 | (Story) => { 60 |
61 | 62 |
63 | } 64 | ] 65 | } 66 | ``` 67 | 68 | - 데코레이터에 Context의 Provier를 설정할 수 있다. 예를 들어 로그인한 사용자의 정보가 있는 `Provider(LoginUserInfoProvider)`를 데코레이터가 소유했다면, Context의 Provider에 의존하는 UI 컴포넌트의 스토리에서도 로그인한 사용자의 정보를 표시할 수 있다: 69 | 70 | ```tsx 71 | export const LoginUserInfoProviderDecorator = ( 72 | Story: PartialStoryFn 73 | ) => ( 74 | 75 | {/* 스토리가 LoginUserInfoProvider를 통해 LoginUserInfo를 참조 */} 76 | 77 | ); 78 | ``` 79 | 80 | - 스토리북의 기능인 play function을 사용하면 인터랙션 할당 상태를 스토리로 등록할 수 있다 81 | - 스토리북의 테스트 러너는 스토리를 실행 가능한 테스트로 변환한다. 이렇게 변환된 스토리는 Jest와 PlayWright에서 실행된다. 이 기능을 활용하여 UI 컴포넌트 테스트로도 활용할 수 있다 -------------------------------------------------------------------------------- /이우혁/chapter1/README.md: -------------------------------------------------------------------------------- 1 | ### 테스트를 작성해야 하는 이유 2 | 3 | - 핵심 기능에 발생할 수 있는 버그를 사전에 발견할 수 있다. 4 | - 리팩토링 시에 생기는 버그를 발견할 수 있다.(리팩토링도 자신감 있게 할 수 있는 선순환) 5 | - 테스트를 하기 쉬운 코드를 작성하여 코드 품질을 올릴 수 있다.(단일 책임, 의존성 관리 등) 6 | - 테스트 코드는 글로 작성된 문서보다 우수한 사양서다.(불필요한 커뮤니케이션 비용 감소) 7 | 8 | ### 테스트를 작성하지 않는 이유 9 | 10 | - 시간 부족(기능 구현, 추가적인 테스트 학습 등) 11 | - 자주 변경되는 요구사항, UI(코드의 생명주기가 길지 않음) 12 | 13 | ### 테스트를 작성하면 시간이 절약되는 이유 14 | 15 | 단기적인 관점에서 보면 테스트 작성은 많은 시간을 소모하지만 장기적인 관점에서 보면 오히려 시간이 절약된다. 16 | 17 | > 규모가 작은 프로젝트는 테스트 코드를 작성하는게 효율이 적거나 오히려 독이 될 수도 있을 것 같다🤔 18 | 19 | ### 팀원들을 설득하는 방법 20 | 21 | `다음부터 테스트를 작성하자` 는 접근법은 성공하기 어렵다. 이해관계자들을 설득하는 것이 쉽지 않다. 22 | 23 | 시간의 흐름에 따라 테스트할 구현 코드가 늘어날수록 테스트 작성 문화를 정착시키는 것은 더 어려웠다. 24 | 25 | > [!TIP] 26 | > 팀에 테스트 코드를 작성하는 문화를 정착시킬 수 있는지 없는지는 초기 단계에서 결정된다. 27 | > 28 | > - 구현 코드가 적을 때는 어떻게 테스트를 작성할지 방침을 세우기 쉽다. 29 | > - 테스트에 익숙하지 않는 멤버들도 커밋된 코드를 참고하면서 테스트를 작성할 수 있다. 30 | -------------------------------------------------------------------------------- /이우혁/chapter10/README.md: -------------------------------------------------------------------------------- 1 | ### E2E 테스트 2 | 3 | - 브라우저 고유 기능과 연동된 UI 테스트 4 | 5 | - 데이터베이스 및 하위 시스템과 연동된 E2E 테스트 6 | 7 | E2E 테스트에서는 위 시스템들을 포함한 전체 구조에서 얼마나 실제와 유사한 상황을 재현할 것인지가 중요한 기준점이 된다. 8 | 9 | ### 브라우저 고유 기능과 연동한 UI 테스트 10 | 11 | 웹 애플리케이션은 브라우저 고유 기능을 사용한다. 아래와 같은 상황들은 `jsdom`에서 제대로 된 테스트를 할 수 없다. 12 | 13 | - 화면 간의 이동 14 | 15 | - 화면 크기를 측정해서 실행되는 로직 16 | - CSS 미디어쿼리를 활용한 반응형 처리 17 | - 스크롤 위치에 따른 이벤트 발생 18 | - 쿠키나 로컬 저장소 등에 데이터를 저장 19 | 20 | UI 테스트(피처 테스트) 21 | 22 | - 브라우저로 실제 상황과 최대한 유사하게 테스트 23 | 24 | ### 데이터베이스 및 서브 시스템과 연동한 E2E 테스트 25 | 26 | E2E 테스트 프레임워크는 UI 자동화 기능으로 실제 애플리케이션을 브라우저 너머에서 조작한다. 27 | 28 | - DB 서버와 연동하여 데이터를 불러오거나 저장한다. 29 | 30 | - 외부 저장소 서비스와 연동하여 이미지 등을 업로드한다. 31 | - 레디스와 연동하여 세션을 관리한다. 32 | 33 | E2E 테스트는 표현, 응용, 영속 계층을 연동하여 검증하므로 실제 상황과 유사성이 높은 테스트로 자리매김했다. 34 | 35 | 반대로 많은 시스템과 연동하기 때문에 실행시간이 길고 불안정하다. 36 | 37 | ### 플레이라이트 38 | 39 | 마이크로소프트가 공개한 E2E 테스트 프레임워크이다. 40 | 41 | 크로스 브라우징을 지원하며 `디버깅 테스트`, `리포터`, `트레이스 뷰어`, `테스트 코드 생성기` 등 다양한 기능이 있다. 42 | 43 | ### 로케이터 44 | 45 | 플레이라이트의 핵심 API이며 현재 페이지에서 특정 요소를 가져온다. 46 | 47 | 플레이라이트도 테스팅 라이브러리의 쿼리와 마찬가지로 신체적, 정신적 특성에 따른 차이 없이 동등하게 정보에 접근할 수 있도록 접근성 기반 로케이터를 우선적으로 사용하는 것을 권장한다. 48 | 49 | **차이점** 50 | 51 | - 대기 시간이 필요한지에 따라 `findByRole` 등을 구분해서 사용하지 않아도 된다는 것이다. 52 | 53 | - 인터랙션은 비동기 함수이기 때문에 `await` 로 인터랙션이 완료될 때까지 기다린 후 다음 인터랙션을 실행하는 방식으로 작동한다. 54 | 55 | ### 플레이라이트 검사 도구를 활용한 디버깅 56 | 57 | E2E 테스트를 작성하다 보면 생각한 것과 다르게 테스트가 통과되지 않을 때가 있다. 58 | 59 | 이럴 때 `플레이라이트 검사 도구` 로 원인을 파악해야 한다. 60 | 61 | 테스트를 실행하는 커맨드에 `--debug` 옵션을 붙이면 `headed` 모드로 테스트가 시작된다. 62 | 63 | - `headed` 모드: 브라우저를 열어서 육안으로 자동화된 UI 테스트를 확인할 수 있는 모드 64 | 65 | ### 불안정한 테스트 대처 방법 66 | 67 | - `실행할 때 마다 데이터베이스 재설정하기`: 일관성있는 결과를 얻으려면 테스트 시작 시점의 상태를 항상 동일해야 한다. 68 | 69 | - `테스트마다 사용자를 새로 만들기`: 테스트에서는 각 테스트를 위해 생성한 사용자를 사용해야 하고 테스트 후에는 테스트용 사용자를 삭제해야 한다. 70 | - `테스트 간 리소스가 경합하지 않도록 주의하기` : 각 테스트에서 매번 새로운 리소스를 작성하도록 해야 한다. 71 | - `빌드한 애플리케이션 서버로 테스트하기` : 빌드한 Next.js 애플리케이션은 개발 서버와 다르게 작동하기 때문에 빌드한 결과물을 테스트해야 한다. 72 | - `비동기 처리 대기하기` : 만약 조작할 요소가 존재하고 문제없이 인터랙션이 할당됐음에도 테스트가 실패한다면 비동기 처리를 제대로 대기하는 확인해야 한다. 73 | - `--debug 로 테스트 실패 원인 조사하기` : 플레이라이트는 실행할 때 `--debug` 옵션을 붙이면 디버거를 실행할 수 있다. 직접 디버거에서 한 줄씩 작동을 확인하면 테스트가 실패하는 원인을 빠르게 찾을 수 있다. 74 | - `CI 환경과 CPU 코어 수 맞추기` : 플레이라이트는 제스트는 코어 수를 명시적으로 지정하지 않으면 실행 환경에서 실행 가능한 만큼 테스트 스위트를 병렬 처리한다. 따라서 병렬 처리되는 숫자는 실행 환경의 CPU 코어 수 때문에 변동된다. 75 | -------------------------------------------------------------------------------- /이우혁/chapter2/README.md: -------------------------------------------------------------------------------- 1 | ### 테스트 피라미드 2 | 3 | (상층부) E2E → 통합 → 단위 (하층부) 4 | 5 | - 상층부: 높은 유사성, 긴 실행 시간, 낮은 안정성 6 | - 하층부: 낮은 유사성, 짧은 실행 시간, 높은 안정성 7 | 8 | → 하층부 테스트의 비중이 높을 수록 더욱 안정적이고 가성비 높은 테스트가 가능하다. 9 | 10 | ### 테스팅 트로피 11 | 12 | FE 개발에서 단일 UI 컴포넌트로 구현되는 기능은 거의 없다. 13 | 14 | 버튼을 누르면(인터랙션) 외부 API를 호출하는 기능도 여러 컴포넌트의 조합으로 구현된다. 15 | 16 | → 사용자 조작을 기점으로 한 통합 테스트 비중이 높을수록 우수한 테스트 전략이다. 17 | 18 | > [!WARNING] 19 | > 20 | > 지나치게 많이 작성한 테스트를 발견했다면 과감하게 줄여야한다. 21 | > 22 | > 이 책에는 수많은 테스트 코드가 있지만 모든 프로젝트에서 이 정도의 테스트 코드가 필요하지 않다.
23 | > 프로젝트에 알맞은 기술 구성은 무엇이며, 어떤 테스트 전략이 프로젝트에 적합한지 항상 되돌아보자. 24 | -------------------------------------------------------------------------------- /이우혁/chapter3/README.md: -------------------------------------------------------------------------------- 1 | ### Jest 2 | 3 | Mock 객체와 코드 커버리지 수집 기능까지 갖춘 메타의 오픈 소스이다. 4 | 5 | > 메타쪽 레포에 없길래 찾아보니깐 OpenJS Foundation 으로 소유권 이전을 했다고 한다😮 6 | 7 | ### test 8 | 9 | ```jsx 10 | test(테스트 명, 테스트 함수) 11 | ``` 12 | 13 | - 첫 번째 인수: 테스트 내용을 잘 나타내는 제목을 짓는다. 14 | - 테스트 코드가 어떤 의도로 작성됐으며, 어떤 작업이 포함되었는지 테스트 명으로 명확하게 표현해야 한다. 15 | - 두 번째 인수: 검증 값이 기대 값과 일치하는지 검증하는 단언문을 작성한다. 16 | - 단언문은 `expect` 함수와 매처(Matcher)로 구성되어 있다. 17 | - 단언문: `expect(검증값).toBe(기대값)` 18 | - 매처: `toBe(기대값)` 19 | 20 | ### 테스트 그룹화 21 | 22 | 연관성 있는 테스트들을 그룹화하고 싶을 때는 `describe` 함수를 사용한다. 23 | 24 | `test` 함수와 유사하게 `describe(그룹명, 그룹함수)` 형식으로 두 개의 매개변수로 구성된다. 25 | 26 | ```jsx 27 | describe("add", () => { 28 | test("1 + 1은 2", () => { 29 | expect(add(1, 1)).toBe(2); 30 | }); 31 | 32 | test("1 + 2는 3", () => { 33 | expect(add(1, 2)).toBe(3); 34 | }); 35 | }); 36 | ``` 37 | 38 | - `test` 는 중첩이 불가능하지만 `describe` 은 중첩이 가능하다. 39 | 40 | ### 예외 발생시키기 41 | 42 | 매개변수 `a` , `b` 는 `0에서 100까지 숫자만 받을 수 있다는 조건`을 추가하면 타입스크립트만으로 커버하기 어렵기 때문에 함수 내부에서 분기처리를 통해 예외를 발생시키면 구현 중에 발생하는 문제를 빠르게 발견할 수 있다. 43 | 44 | ```jsx 45 | const add = (a: number, b: number) => { 46 | if (a < 0 || a > 100) throw new Error("0 ~ 100 사이의 값을 입력해 주세요."); 47 | if (b < 0 || b > 100) throw new Error("0 ~ 100 사이의 값을 입력해 주세요."); 48 | return a + b; 49 | }; 50 | ``` 51 | 52 | ### 예외 발생 검증 테스트 53 | 54 | ```jsx 55 | expect(예외가 발생하는 함수).toThrow(에러 세부 사항 검증); 56 | // 에러 세부 사항 검증은 옵셔널이다. 57 | // 정규 표현식, 문자열, 에러 객체, 에러 클래스 등 테스트가 가능하다. 58 | ``` 59 | 60 | 예외가 발생하는 함수는 화살표 함수로 감싸서 작성한 함수다. 61 | 62 | 화살표 함수를 사용하면 함수에서 예외가 발생하는지 검증할 수 있다. 63 | 64 | ```jsx 65 | // ⛔️ 잘못된 작성법 66 | expect(add(-10, 100)).toThrow("0 ~ 100 사이의 값을 입력해 주세요."); 67 | 68 | // ✅ 올바른 작성법 69 | expect(() => add(-10, 100)).toThrow("0 ~ 100 사이의 값을 입력해 주세요."); 70 | ``` 71 | 72 | ### Matchers 73 | 74 | | Method | Description | 75 | | ---------------------- | -------------------------------------------------------------- | 76 | | toBe | 원시 값의 정확한 일치를 테스트 | 77 | | toEqual | 객체나 배열의 모든 속성을 재귀적으로 비교 | 78 | | not | Matcher의 결과를 반대로 테스트 | 79 | | toBeNull | null 값인지 테스트 | 80 | | toBeUndefined | 값이 정의되어 있는지 테스트 | 81 | | toBoTruthy | true로 평가되는 값인지 테스트 | 82 | | toBoFalsy | false로 평가되는 값인지 테스트 | 83 | | toBeGreaterThan | 주어진 값보다 큰지 테스트 | 84 | | toBeGreaterThanOrEqual | 주어진 값보다 크거나 같은지 테스트 | 85 | | toBeLessThan | 주어진 값보다 작은지 테스트 | 86 | | toBeLessThanOrEqual | 주어진 값보다 작거나 같은지 테스트 | 87 | | toBeCloseTo | 부동 소수점 숫자를 비교할 때 사용 | 88 | | toMatch | 문자열이 정규 표현식과 일치하는지 테스트 | 89 | | toContain | 배열이나 순회 가능한 객체에 특정 항목이 포함되어 있는지 테스트 | 90 | | toThrow | 함수가 예외를 던지는지 테스트 | 91 | | toHaveProperty | 객체가 특정 속성을 가지고 있는지 테스트 | 92 | | toHaveBeenCalled | 모의 함수가 호출되었는지 테스트 | 93 | | toHaveBeenCalledWith | 모의 함수가 특정 인수로 호출되었는지 테스트 | 94 | 95 | 이외에도 더 많은 Matcher가 존재한다. 96 | 97 | ### 비동기 처리 테스트 98 | 99 | 1. `then` 에 전달될 함수에 단언문을 작성하는 방법 100 | 101 | ```jsx 102 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", () => { 103 | return wait(50).then((duration) => { 104 | expect(duration).toBe(50); 105 | }); 106 | }); 107 | ``` 108 | 109 | 해당 인스턴스를 테스트 함수의 반환 값으로 `return`하면 `Promise`가 처리 중인 작업이 완료될 때까지 테스트 판정을 유예한다. 110 | 111 | 2. `resloves` 매처에 사용하는 단언문을 `return` 하는 방법 112 | 113 | ```jsx 114 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", () => { 115 | return expect(wait(50)).resolves.toBe(50); 116 | }); 117 | ``` 118 | 119 | `wait` 함수가 `reslove` 됐을 때의 값을 검증한다. 120 | 121 | 3. 테스트를 함수를 `async` 함수로 만들고 함수 내에서 `Promise`가 완료될 때까지 기다리는 방법 122 | 123 | ```jsx 124 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", async () => { 125 | await expect(wait(50)).resolves.toBe(50); 126 | }); 127 | ``` 128 | 129 | 테스트 함수를 `async` 함수로 만들고 함수 내에서 `Promise` 가 완료될 때까지 기다리는 방법이다. 130 | 131 | 4. 검증 값인 `Promise` 가 완료되는 것을 기다린 뒤 단언문을 실행하는 방법 132 | 133 | ```jsx 134 | test("지정 시간을 기다린 뒤 경과 시간과 함께 resolve된다", async () => { 135 | expect(await wait(50)).toBe(50); 136 | }); 137 | ``` 138 | 139 | `async/await` 함수를 사용하면 비동기 처리가 포함된 단언문이 여러 개일 때 한 개의 테스트 함수 내에서 정리할 수 있는 장점도 있다. 140 | 141 | > 🤔 나는 4번 방식이 가장 간단하고 직관적이라는 생각이 드는 것 같다. 142 | 143 | ### Reject 검증 테스트 144 | 145 | 1. `Promise`를 `return` 하는 방법 146 | 147 | ```jsx 148 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", () => { 149 | return timeout(50).catch((duration) => { 150 | expect(duration).toBe(50); 151 | }); 152 | }); 153 | ``` 154 | 155 | `catch` 메서드에 전달할 함수에 단언문을 작성한다. 156 | 157 | 2. `rejects` 매처를 사용하는 단언문을 활용하는 방법 158 | 159 | ```jsx 160 | // 단언문 return 161 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", () => { 162 | return expect(timeout(50)).rejects.toBe(50); 163 | }); 164 | 165 | // async/await 사용 166 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", async () => { 167 | await expect(timeout(50)).rejects.toBe(50); 168 | }); 169 | ``` 170 | 171 | 단언문을 `return` 하거나 `async/await` 를 사용한다. 172 | 173 | 3. `try-catch` 문을 사용하는 방법 174 | 175 | ```jsx 176 | test("지정 시간을 기다린 뒤 경과 시간과 함께 reject된다", async () => { 177 | expect.assertions(1); // 반드시 1개의 assertion(expect)이 실행되어야 함 178 | try { 179 | await timeout(50); 180 | } catch (err) { 181 | expect(err).toBe(50); 182 | } 183 | }); 184 | ``` 185 | 186 | `Unhandled Rejection` 을 `try` 블록에서 발생시키고, 발생한 오류를 `catch` 블록에서 받아 단언문으로 검증한다. 187 | 188 | > [!TIP] 189 | > Promise 거부(reject)의 두 가지 상태 190 | > 191 | > | State | Description | 192 | > | ------------------- | -------------------------------------------------------- | 193 | > | Handled Rejection | reject된 Promise를 .catch() 또는 try-catch로 처리한 상태 | 194 | > | Unhandled Rejection | reject된 Promise를 아무도 처리하지 않은 상태 | 195 | 196 | > 🤔 이번에도 3번 방식이 가장 직관적인 것 같다. 197 | 198 | > [!WARNING] 199 | > 200 | > 비동기 처리를 테스트할 때 테스트 함수가 `동기 함수` 인 경우 반드시 단언문을 `return` 해야한다. 201 | > 202 | > 이 같은 실수를 하지 않으려면 비동기 처리가 포함된 부부을 테스트할 때는 다음과 같은 원칙을 가지고 접근해야 한다. 203 | > 204 | > - 비동기 처리가 포함된 부분을 테스트할 때는 테스트 함수를 `async` 함수로 만든다. 205 | > - `.resloves`, `rejects` 가 포함된 단언문은 `await` 한다. 206 | > - `try-catch` 문의 예외 발생을 검증할 때는 `expect.assertions` 를 사용한다. 207 | -------------------------------------------------------------------------------- /이우혁/chapter4/README.md: -------------------------------------------------------------------------------- 1 | ### Mock 객체를 사용하는 이유 2 | 3 | 테스트는 실제 실행 환경과 유사할수록 재현성이 높다. 하지만 재현성을 높이다보면 실행 시간이 오래 걸리거나 환경 구축이 어려워지는 경우가 있다. 4 | 5 | 지금 테스트하는 대상은 웹 API 자체가 아니라 취득한 데이터에 대한 처리라는 것을 명심하자. 6 | 7 | 웹 API 서버가 테스트 환경에 반드시 필요한 것은 아니다. 이 때 취득한 데이터 대역으로 사용하는 것이 `Mock 객체(테스트 더블, Test Double)` 이다. 8 | 9 | ### Mock 객체 용어 10 | 11 | `스텁`, `스파이` 등은 Mock 객체를 상황에 따라 세분화한 객체의 명칭이다. 12 | 13 | 이러한 용어는 개발 언어에 상관없이 테스트 자동화 관련 문헌에서 정의한 용어이다. 14 | 15 | ### 스텁 16 | 17 | 테스트 중에 실제 객체 대신 사용되는 대역으로 미리 준비된 결과 값을 반환하도록 만들어진 객체이다. 18 | 19 | - 테스트에 필요한 최소한의 기능만 구현 20 | - 미리 정의된 응답만 반환 21 | - 상태 검증에 중점 22 | 23 | ```jsx 24 | // 계산기 스텁 25 | const calculatorStub = { 26 | add: (a, b) => 5, // 항상 5를 반환하도록 고정 27 | }; 28 | 29 | test("계산기 스텁 테스트", () => { 30 | const result = calculatorStub.add(2, 3); 31 | expect(result).toBe(5); 32 | }); 33 | ``` 34 | 35 | ### 스파이 36 | 37 | 실제 객체의 메서드 호출을 감시하고 추적하는 역할을 한다. 실제 구현은 그대로 두면서 메서드 호출 정보를 기록한다. 38 | 39 | - 메서드 호출 횟수 추적 가능 40 | - 호출 시 전달된 인자 확인 가능 41 | - 실제 메서드 동작은 유지 42 | 43 | ```jsx 44 | test("스파이 테스트", () => { 45 | const calculator = { 46 | add: (a, b) => a + b, 47 | }; 48 | 49 | const spyFn = jest.spyOn(calculator, "add"); 50 | 51 | const result = calculator.add(2, 3); 52 | 53 | expect(spyFn).toBeCalledTimes(1); // 호출 횟수 확인 54 | expect(spyFn).toBeCalledWith(2, 3); // 전달된 인자 확인 55 | expect(result).toBe(5); // 실제 결과값 확인 56 | }); 57 | ``` 58 | 59 | > [!NOTE] 60 | > 61 | > Jest의 API는 `xUnit 테스트 패턴` 의 용어 정의를 충실하게 따르지 않는다. 62 | > 63 | > 이 책에서는 앞서 설명한 `스텁` , `스파이` 로서 사용하는 명확한 이유가 있을 때는 `스텁` , `스파이` 라고 구분하고 여러가지 이유로 사용될 때는 `Mock 객체` 라고 한다. 64 | 65 | ### 라이브러리 대체하기 66 | 67 | ```jsx 68 | jest.mock("next/router", () => require("next-router-mock")); 69 | ``` 70 | 71 | 위 코드는 `next/router` 라는 의존 모듈 대신 `next-router-mock` 이라는 라이브러리를 적용한다. 72 | 73 | ```jsx 74 | jest.mock("./greet", () => import("./test")); 75 | ``` 76 | 77 | 이렇게 `dynamic import`하면 정상적으로 모킹이 되지 않는다. 78 | 79 | 그 이유는 `require` 는 동기적으로 즉시 실행되는데, `dynamic import` 는 Promise를 반환하는 비동기 작업이기 때문이다. 80 | 81 | `jest.mock` 은 모듈이 로드되기 전에 실행되어야 하지만 `dynamic import` 는 모듈을 나중에 가져오기 때문에 정상적으로 모킹이 되지 않는다.(`jest.mock` 은 호이스팅되어 실행된다.) 82 | 83 | ### 설정과 파기 84 | 85 | 테스트를 실행하기 전에 공통으로 설정해야 할 작업이 있거나 테스트 종료 후에 공통으로 파기 하고 싶은 작업이 있는 경우에 사용된다. 86 | 87 | - 설정 작업: `beforeAll` , `beforeEach` 88 | - 파기 작업: `afterAll` , `afterEach` 89 | 90 | ```jsx 91 | describe("설정 및 파기 타이밍", () => { 92 | // 외부 블록 93 | beforeAll(() => console.log("1 - beforeAll")); // 테스트 시작 전 94 | 95 | beforeEach(() => console.log("1 - beforeEach")); // 테스트 케이스 전 96 | test("", () => console.log("1 - test")); // 테스트 97 | afterEach(() => console.log("1 - afterEach")); // 테스트 케이스 후 98 | 99 | describe("Scoped / Nested block", () => { 100 | // 내부 블록 101 | beforeAll(() => console.log("2 - beforeAll")); // 테스트 시작 전 102 | 103 | beforeEach(() => console.log("2 - beforeEach")); // 테스트 케이스 전(외부 -> 내부) 104 | test("", () => console.log("2 - test")); // 테스트 105 | afterEach(() => console.log("2 - afterEach")); // 테스트 케이스 후(내부 -> 외부) 106 | 107 | afterAll(() => console.log("2 - afterAll")); // 테스트 종료 후 108 | }); 109 | 110 | afterAll(() => console.log("1 - afterAll")); // 테스트 종료 후 111 | }); 112 | ``` 113 | 114 | 1. 시작 단계 115 | - `1 - beforeAll` : 가장 먼저 외부 블록의 모든 테스트 전에 한 번 실행 116 | 2. 첫 번째 테스트 117 | - `1 - beforeEach` : 첫 번째 테스트 직전 118 | - `1 - test` : 첫 번째 테스트 119 | - `1 - afterEach` : 첫 번째 테스트 직후 120 | 3. 내부 블록 시작 121 | - `2 - beforeAll` : 내부 블록의 테스트 시작 전 122 | 4. 두 번째 테스트 123 | - `1 - beforeEach` : 외부 블록의 `beforeEach` 가 먼저 실행 124 | - `2 - beforeEach` : 내부 블록의 `beforeEach` 가 다음 실행 125 | - `2 - test` : 내부 블록의 테스트 126 | - `2 - afterEach` : 내부 블록의 `afterEach` 가 먼저 실행 127 | - `1 - afterEach` : 외부 블록의 `afterEach` 가 다음 실행 128 | 5. 정리 단계 129 | - `2 - afterAll` : 내부 블록의 모든 테스트 완료 후 130 | - `1 - afterAll` : 마지막으로 외부 블록의 모든 테스트 완료 후 131 | 132 | > [!IMPORTANT] 133 | > 134 | > `beforeAll` , `afterAll` : 해당 `describe` 블록 내의 모든 테스트가 실행되기 전/후에 한 번만 실행 135 | > 136 | > `beforeEach` , `afterEach`: 각 테스트 케이스 전/후 마다 실행 137 | > 138 | > 중첩된 `describe` 블록에서는 외부 → 내부 순으로 실행되고, 139 | > 140 | > `afterEach` 는 내부 → 외부 순으로 실행된다. 141 | 142 | > 어우 알아야 할 메서드가 생각보다 많군요.. 많이 써봐야 익숙해지겠네요… 143 | -------------------------------------------------------------------------------- /이우혁/chapter5/README.md: -------------------------------------------------------------------------------- 1 | ### 대표적인 UI 컴포넌트 기능 2 | 3 | - 데이터를 렌더링하는 기술 4 | - 사용자의 입력을 전달하는 기능 5 | - 웹 API와 연동하는 기능 6 | - 데이터를 동적으로 변경하는 기능 7 | 8 | 이 기능들은 테스트 프레임워크와 라이브러리를 통해 `의도한 대로 작동하고 있는가` 와 `문제가 생긴 부분은 없는가` 를 확인해야 한다. 9 | 10 | --- 11 | 12 | ### 웹 접근성 테스트 13 | 14 | 디자인 대로 화면이 구현됐고, 마우스 입력에 따라 정상적으로 작동한다면 품질에 문제가 없다고 생각하기 때문에 웹 접근성은 의도적으로 신경 써야만 알 수 있는 부분이다. 15 | 16 | → 많은 사람들이 웹 접근성을 준수하지 않는 대표적인 예시가 `체크 박스` 이다. 17 | 18 | ```jsx 19 | <> 20 | 21 |