풀스택 서버리스 프로젝트 with React - 9. TDD - 테스트 주도 개발

2024. 1. 4. 19:57개인노트-인강

사이드 프로젝트:10개 기술스택으로 구현하는 풀스택 서버리스 프로젝트 with React
Part 3. 프로젝트 설계하기
Ch03. TDD - 테스트 주도 개발

 

# TDD(Test-Driven Development)를 활용하기

- "Test-Driven Development"의 약자 한국어로는 "테스트 주도 개발"

 

- 디자인 → 로직 구현 → 테스트 ❌
- 디자인 → 테스트 코드 → 로직 구현 ✅

 

- 소프트웨어를 동작시키기 위한 로직을 구현하기 전에 테스트 코드를 먼저 구현하는 것을 프로세스화 한 개발 방법(론 중의 하나이다.)

## 테스트 코드가 왜 중요한가요?

- 작성한 코드가 의도적으로 동작하는지 수시로 빠르게 검증할 수 있다. 매번 서버를 돌려서 수동적으로 input/output을 검증하는 비효율적인 방법에서 벗어날 수 있음.
- 리팩토링을 할 때 → 리팩토링 후에도 소프트웨어가 여전히 같은 기능을 제공할 수 있도록 안정망 역할
- 잘 작성된 테스트 코드는 소프트웨어의 명세서가 되기도 함.

기능이 정상 동작할 경우, 예외를 일으킬 경우 등 코드를 직접 읽기보다 가독성이 높은 human-language로 테스트 케이스를 읽는 것이 더 빠르게 이해할 수 있습니다.
👩‍🏫
 저는 동료 코드 리뷰 시에도 테스트 코드를 먼저 읽으면서 해당 기능/객체에 대해 빠진 테스트가 있는지, 이 테스트들이 논리적으로 말이 되는지 보곤 합니다.

 

- 매 배포 전, 전체 테스트 코드를 돌려 품질이 보증된 소프트웨어를 일정하게 제공할 수 있다.

## 그렇다면 TDD는 왜 중요하죠?

- 테스트 코드를 먼저 작성하지 않으면, 의식의 흐름대로, 기능 단위로, 혹은 이곳 저곳.. 방대한 양의 코드를 작성해나가기 시작한다. 문제가 발생시 확인해야 할 코드의 범위가 넓어진다
- 테스트 코드를 먼저 작성하면, 예외적인 상황을 미리 고민하고 정리하는 과정을 통해 버그가 생기는 것을 사전에 방지 할 수 있다. 즉, 버그를 만들 확률 🔻
- 객체 지향적 설계도 가능. 각 객체를 테스트하는 관점에서 특정 객체가 어떻게 동작해야 할지, 객체 간 어떻게 메시지를 주고받아야 할지 먼저 논리적으로 생각해볼 수 있기 때문
- 테스트 코드를 작성하는 과정에서 기능 구현을 위한 설계 요소를 고민하게 되며 구조적으로 더 나은 코드를 생산할 수 있다
- 테스트 코드가 추가될 때마다 검증되는 범위가 넓어지므로, 소프트웨어의 품질을 높일 수 있다

test('그룹명을 입력하지 않고, 저장할 경우 에러 메세지 노출', () => {
    render(<CreatingGroup/>)
    
    const saveButton = screen.getByText('저장')
    fireEvent.click(saveButton)
    
    await waitFor(() => {
        expect(getByText('그룹명을 입력해 주세요')).toBeInTheDocument()
    })
})

## TDD는 어떻게 사용하나요?

테스트 주도 개발 주기

1. 실패하는 테스트 작성

test('그룹명을 입력하지 않고, 저장할 경우 에러 메시지 노출', () => {
    expect(false).toBe(true)
})

- 테스트 시나리오를 메소드화 한다는 것에 촛점을 맞춘다.
- 테스트 메소드명은 ‘어떤 것을 테스트하고자 하는지' 목적을 담자.
- 핵심 로직을 검증하는 단계가 아니므로, 우선 테스트는 실패하도록 한다.

2. 테스트 통과 시키기

test('그룹명을 입력하지 않고, 저장할 경우 에러 메세지 노출', () => {
    render(<CreatingGroup/>)
    
    const saveButton = screen.getByText('저장')
    fireEvent.click(saveButton)
    
    await waitFor(() => {
        expect(getByText('그룹명을 입력해 주세요')).toBeInTheDocument()
    })
})

- 테스트를 통과 시킬 만큼의 코드만 구현하는 과정.
- 목(Mock) 객체 활용
    - 목(Mock) 객체 = 가짜 객체
    - 소프트웨어란 결국, 객체 간의 통신을 통해 사용자에게 기능을 제공하는 것인데,
    - 한 객체를 테스트 하기 위해서 엮인 모든 객체(혹은 외부와의 통신)를 다 initialize 하고 기능을 테스트 하기란 비효율 적임
    - 객체 간의 의존성을 다 충족 시키는 것에 집중하다 보면 실제 객체의 기능 테스트 본질도 흐려짐
    - 따라서 대상 객체에서 직접 참조하는 객체들은 mock 객체로 대체 → 발생하는 이벤트에 대해 대상 객체가 mock 객체와 어떻게 소통할 지 직접 지정할 수 있다 → 대상 객체를 테스팅 하는데에만 집중할 수 있다.

3. 리팩토링

- 프로덕션 코드의 구조를 개선하는 과정
- 2번 까지는 테스트를 통과시키기 위해 최소한의 코드를 작성했을 것이다. 이제는 객체 지향적으로, 가독성을 높일 수 있도록 하기 위해 구조적 개선을 하는 데에 집중
- 리팩토링 틈틈이 테스트 코드를 돌려 계속 성공 시킴으로써, 리팩토링 후에도 동일한 기능을 제공 보장

# 테스트 작성의 정석 - 의미 있는 테스트 작성하기

## 테스트의 종류

### 단위 테스팅(Unit testing)

- 하나의 모듈/컴포넌트/클래스가 기대한대로 동작하는지, 제공하는 기능들을 테스트
- 대상 컴포넌트에서 의존하는 일부 대상은 목(mock) 객체를 이용하여 테스트 하기 편한 환경을 구축할 수 있다
- 개발자가 작성
- 자동화된 테스팅

### 통합 테스팅(Integration testing)

- 두 개 이상의 모듈이 잘 연동/연결이 되었는지 테스팅. 모듈 간에 발생하는 에러 검증
    - e.g. 3rd party API를 호출하면 어떤 응답을 기대하는지 테스팅
- 개발자가 작성
- 자동화된 테스팅

### E2E 테스팅(End-to-end testing)

- 실제 사용자가 이용하는 환경과 최대한 유사하게 만들어 사용자의 경험을 전반적으로 테스팅
- 사용자의 입장에서 시스템이 기능을 올바르게 제공하는지 테스팅
    - e.g. 시나리오: 사용자가 ‘그룹 생성하기' 버튼을 클릭하면 해당 페이지로 리다이렉팅 된다.
- QA 조직이 따로 있을 경우, 개발자가 아닌 QA 전문가가 작성하곤 한다
- 자동화된 테스팅

### 인수 테스팅(Acceptance testing)

- 시스템이 주어진 요구사항을 잘 충족하는지 테스팅
- 자동화된 테스팅 ❌. 사람이 수동으로 테스팅

## 테스트 작성 순서

단위 테스트 ➡️ 통합 테스트 ➡️ E2E 테스트

## 테스트의 영역

- 프론트 엔드, 백엔드 등

## 의미 있는 테스트 작성하기

### 반드시 테스트 해야할 것

✅ 사용자 요구사항(user requirement)이 모두 테스트 케이스화 되어있는가?
✅ 백엔드의 인터페이스를 테스트할 경우, 잘못된 input을 입력했을 때, 예상된 응답을 내려주는가? (에러코드 vs Exception을 throw)
✅ 프론트엔드에서 사용자가 이용할 기능이 동작하는가?
- 기능 테스트에 초점을 맞춰야 합니다!
- 페이지에 중요한 버튼이 렌더링 되는가?
- 해당 버튼을 클릭하면 예상한대로 동작하는가?

### 테스트 하지 않아도 되는 것

❌ 모든 라인을 반드시 테스트 해야 겠다! 
❌ 페이지가 반응성(responsiveness web)인지 테스트 해야겠다!
- e.g. “PC 에서는 버튼이 화면 중앙에, 모바일에서는 버튼 위치가 화면 하단에 있어야 한다”

## 마무리

구현하고자 하는 기능이 있을 때,
어디까지 테스트 케이스를 작성 해야 하는가? 에 대한 고민은 항상 있습니다.
고민하고 항상 ‘이게 맞는가? 왜 필요한가? 왜 필요하지 않은가?’에 대한 질문을 끊임없이 하세요.
여러분의 성장에 좋은 거름이 될 것입니다.

# 마무리 정리 Recap

## 시스템 설계

- 시스템의 요구사항을 충족하기 위해 필요한 아키텍처, 인터페이스 및 데이터를 정의하는 과정.
- 소프트웨어를 구성하는 요소들 관의 관계를 정의하고 동작 메커니즘을 표현하기 위한 구조체.

### 설계 유형

- 아키텍처 설계: 소프트웨어의 전체 구조를 high-level에서 기술. 구성 요소를 정의하고 요소들 간의 관계를 정의
- 자료구조 설계:  소프트웨어의 요구사항을 충족시키기 위해 필요한 요소들을 자료구조로 변환하여 설계하는 과정
- 인터페이스 설계: 사용자와 소프트웨어 간, 소프트웨어를 구성하는 구성 요소 간 어떻게 통신하는지 프로토콜과 주고받을 데이터 내용 등을 설계하는 과정

### 설계 과정

 

## 아키텍처 다이어그램

- 아키텍처 설계를 위해서는 아키텍처 다이어그램을 그릴 수 있습니다.

- 소프트웨어를 구성하는 구성 요소 간의 상호 작용 및 의존성을 high-level에서 가시화 한 다이어그램

많은 정보를 담고 있지는 않다. 봤을 때, 이 소프트웨어는 어떤 구성요소들끼리 통신을 한다. 정도로만 알 수 있다.

  • 1. 구성 요소 나열
    • 클라이언트
    • 서버/서비스
    • 데이터베이스, 스토리지
  • 2. 구성 요소 간 상호 작용 표기 ➡️ ⬅️

## 시퀀스 다이어그램

- 객체 간의 상호 작용을 시간 순으로 시각화 한 다이어그램

순서가 중요할 때, 복잡한 알고리즘을 도식화 할 때. 사용하기 좋다

  1. 객체/참여자 나열(User, Web Client, DutchPay Server, Group)
    1. 사용자, 데이터베이스, 시스템/서비스, 클래스
  2. 객체/참여자 간 메시지를 순서대로 정의
    1. 동기 메시지 전송
    2. 비동기 메시지 전송
    3. 자체 메시지 전송
    4. 동기 반환
    5. 비동기 반환
  3. (옵션) 메시지를 전송하는 조건이나 반복이 필요할 경우 명시
    1. [condition] or Options = If
    2. Alt = If/else
    3. Loop = While

## 클래스 다이어그램

- 한 시스템을 구성하는 클래스들의 구조에 촛점을 맞춘, 속성(attribute), 메소드(method)를 시각화 한 다이어그램

1. 클래스 정의 ➡️ 속성 + 메소드
2. 클래스 간 관계 정의 ➡️ 상속 vs 조합

## 기술셋을 선정하는 기준

1. 시스템의 요구사항을 잘 충족시키기 위해 필요한 것들 리스팅
2. 후보군 조사 - 리서치 하는 시간을 충분히 가질 것!
3. 비교 테이블 생성 및 분석
4. 팀 내 토론 및 결정

## 더치페이 서비스에서 사용할 기술 스택 소개

### AWS Amplify

모바일/웹 어플리케이션을 빠르게 구성+빌드+배포+운영까지 모든 라이프사이클을 한 곳에서 관리할 수 있도록 통합해둔 AWS의 풀스택 개발 통합 솔루션 서비스

### React

컴포넌트 기반, 사용자 인터페이스를 만들기 위한 Javascript 라이브러리

 

React 컴포넌트 잘 설계하기

1. 페이지별로 컴포넌트를 정의한다
2. UI 디자인 기반, 공통된 요소들이 있는지 파악 후 컴포넌트 화
3. 사용할 만한 컴포넌트 디자인 패턴이 있다면 적용
    - Container 패턴: UI를 렌더링하는데에 집중하는 컴포넌트, 데이터 불러오기 등의 비지니스 로직을 담아둘 Container 컴포넌트로 나눠 설계
    - Provider 패턴: 컴포넌트 내 props drilling (프로퍼티 내리꽂기)을 방지하고자 생긴 패턴. [React Context](https://www.notion.so/3-Class-diagram-for-a3bc66c28d0242a59fe335617ceaf65b?pvs=21)나 Redux/Recoil과 같이 중앙 상태 관리를 이용하여 데이터를 저장해 두고, 필요한 모든 컴포넌트에서 이 데이터에 접근 가능하도록 한 패턴.
4. 각 컴포넌트에 최소한으로 필요한 props는 무엇일지 정의
    - 어떤 데이터들을 컴포넌트들 끼리 주고받아야 할 지 생각해보기
    - 각 컴포넌트 별로 state/props를 미리 생각해볼 것
5. 한 컴포넌트가 하나의 책임만 가지고 있는지 확인(Single Responsibility Principle)
    - 더치 페이 예시 - 한 컴포넌트 내에서 그룹 생성하는 것 뿐만 아니라 그룹을 삭제하는 UI/로직 까지 담고 있다면 이 컴포넌트는 너무 많은 것을 핸들링 하려고 하는 것 ➡️  쪼갭시다!

## TDD (Test-Driven Development)

소프트웨어를 동작시키기 위한 로직을 구현하기 전에 테스트 코드를 먼저 구현하는 것을 프로세스화 한 개발 방법

테스트 주도 개발 주기

- 테스트 코드를 먼저 작성하면, 예외적인 상황을 미리 고민하고 정리하는 과정을 통해 버그가 생기는 것을 사전에 방지 할 수 있다. 즉, 버그를 만들 확률 🔻
- 구조적으로 더 나은 코드 생산 가능
- 소프트웨어의 품질을 높일 수 있음

### 테스트의 종류

단위 테스팅(Unit testing)

하나의 모듈/컴포넌트/클래스가 기대한대로 동작 하는지, 제공하는 기능들을 테스트

 

통합 테스팅(Integration testing)
두 개 이상의 모듈이 잘 연동/연결이 되었는지 테스팅. 모듈 간에 발생하는 에러 검증

 

E2E 테스팅(End-to-end testing)
실제 사용자가 이용하는 환경과 최대한 유사하게 만들어 사용자의 경험을 전반적으로 테스팅
사용자의 입장에서 시스템이 기능을 올바르게 제공하는지 시나리오 기반의 테스팅

 

인수 테스팅(Acceptance testing)
시스템이 주어진 요구사항을 잘 충족 하는지 수동으로 수행하는 테스팅.

 

## 의미 있는 테스트 작성하기

반드시 테스트 해야할 것

✅ 사용자 요구사항(user requirement)이 모두 테스트 케이스화 되어있는가?
✅ 정상적인 인풋이 들어 왔을 때 어떻게 응답하는가?
✅ 엣지케이스! 정상적이지 않은 인풋이 들어 왔을 때 어떻게 응답하는가?
✅ 사용자가 이용할 기능이 동작하는가?

- 기능 테스트에 초점을 맞춰야 합니다!
- 페이지에 중요한 버튼이 렌더링 되는가?
- 해당 버튼을 클릭하면 예상한대로 동작하는가?