1. Frontend 관점에서의 Test의 목적과 이점
1-1 안정성과 신뢰성 확보 : 정기적인 테스트를 통해 개발된 애플리케이션이 안정적으로 작동하며,
예상치 못한 사용자의 액션에도 견딜 수 있고 정상적으로 동작하는지를 확인
1-2 코드 동작의 테스트 : 코드가 로직대로 정상적으로 제대로 동작하는지에 대한 검증
1-3 상호 작용 테스트 : 사용자와의 상호작용, 클릭, 스크롤, 입력 등이 정상적으로 동작하는지를 검증
1-4 성능 평가 : 애플리케이션의 로딩시간, 반응속도 등 성능 지표를 평가, 더 좋은 사용자 경험을 위하여 개선점을 식별할 수 있는 것
1-5 크로스 브라우징 호환성 : 다양한 웹 브라우저와 디바이스에서 애플리케이션이 일관된 방식으로 작동하는지를 테스트
1-6 인터페이스 검증 : 프론트엔드 테스트가 사용자 인터페이스가 설계 사양과 일치 하는지를 확인
2. 소프트웨어 테스트의 7원칙
1. 테스트는 결함이 존재함을 밝히는 활동이다.
소프트웨어는 필연적으로 결함이 존재할 수밖에 없다.
2. 완벽한 테스팅을 불가능하다.
매우 단순한 소프트웨어가 아닌 이상, 내부조건,입력값, 타이밍에 대한 모든 조합을 확인할 수 없다.
따라서 테스트 대상의 리스크 분석 후에 가장 중요한 부분을 중점으로 테스팅 리소스를 투입하여야 한다.
3. 테스팅은 개발 초기 단계에서부터 시작해야한다.
요구 사항 분석, 설계 단계에서 발견되는 문서 상의 결함은 나중에 가서 코딩 작업 이후에 발견되는 결함에 비해
훨씬 간단하게 해결이 가능하다.
4. 결합 집중
대부분의 버그와 결함은 소수의 특정 모듈에 의해 집중되는 경향이 있다.
- 자체적으로 복잡한 로직을 가진 모듈
- 소프트웨어의 다른 부분과 복잡한 상호 작용을 하는 모듈
- 개발 난이도가 높거나 최신 기술을 사용하는 모듈
-크기가 큰 모듈
5. 살충제 패러독스
동일한 테스트 케이스를 반복해서 수행하는 경우 어느 일정 수행 시간 이상부터는 더 이상 새로운 결함을 찾아낼 수 없다는 것입니다.
6. 테스트는 정황에 따라 이루어져야한다.
소프트웨어의 종류나 목표 등에 따라 해당 소프트웨어 맞는 fit 한 테스트 방식이 적용되어야 한다.
조직 문화, 사용자의 기대, 테스팅에 필요한 기반 환경, 예산, 출시 일정 등이 고려되어야 한다.
7. 오류 부재의 귀변
거의 모든 결함을 찾아 제거하였다고 하여도 사용자의 요구 또는 비즈니스 목적을 충족 시키지 못하는 경우 품질이 높다고 할 수 없다.
1. Unit Testing(단위 테스트 , 유닛 테스트)
테스트 피라미드에서 가장 아래층
개별 컴포넌트, 함수, 메서드가 예상대로 작동하는지를 검증하는 테스트
개별적인 컴포넌트 ,메소드, 유틸리티 함수가 정확하게 동작하는지를 확인함으로써 전체 애플리케이션의 신뢰성을 높임
-개발 초기단계에서 버그를 신속하게 발견하고, 수정할 수 있음
- 기존 코드를 변경하거나 개선할 때, 단위 테스트가 기존 기능의 올바른 동작을 보장함
- 단위 테스트는 코드의 기능을 설명하는데 도움이 되며, 새로운 개발자가 코드베이스를 이해하는데 기여
단위 테스트 도구들
jest, react testing library , vitesst
import React from 'react';
const Button = ({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
};
export default Button;
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Button from './Button';
test('버튼 클릭 시 콜백 함수가 호출되어야 함', () => {
const handleClick = jest.fn();
const { getByText } = render(<Button onClick={handleClick}>클릭</Button>);
fireEvent.click(getByText('클릭'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
ex2) 유틸리티 함수 테스트
export function formatDate(date) {
return date.toISOString().split('T')[0];
}
import { formatDate } from './formatDate';
test('날짜를 YYYY-MM-DD 형식의 문자열로 변환해야 함', () => {
const date = new Date('2023-10-11T00:00:00Z');
expect(formatDate(date)).toBe('2023-10-11');
}
2. Integration Testing(통합 테스트)
테스트 피라미드의 중간층
여러 컴포넌트와 모듈이 함께 올바르게 작동하는지?
단위 테스트로 검증된 컴포넌트들이 함께 조합될 때 발생할 수 있는 문제들을 식별
시스템 안정성 : 여러 UI컴포넌트나 모듈이 통합되어 예상대로 작동하는지 확인
인터페이스 오류 발견 : 컴포넌트 간의 데이터 전달과 상호작용에서 발생할 수 있는 오류 식별
데이터 흐름 검증 : 상태 관리나 api 호출 등에서 데이터가 올바르게 전달되고 처리되는지 검증
통합 테스트 도구 - 유닛 테스트와 거의 동일
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
};
export default Counter;
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('버튼 클릭 시 카운트가 증가해야 함', () => {
const { getByText } = render(<Counter />);
const counterText = getByText('현재 카운트: 0');
const button = getByText('증가');
fireEvent.click(button);
expect(counterText.textContent).toBe('현재 카운트: 1');
});
import React, { useEffect, useState } from 'react';
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then((res) => res.json())
.then((data) => setUsers(data));
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import UserList from './UserList';
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve([
{ id: 1, name: '홍길동' },
{ id: 2, name: '김철수' },
]),
})
);
});
test('사용자 목록을 렌더링해야 함', async () => {
const { getByText } = render(<UserList />);
await waitFor(() => {
expect(getByText('홍길동')).toBeInTheDocument();
expect(getByText('김철수')).toBeInTheDocument();
});
});
3. E2E 테스트
테스트 피라미드의 최상단
실제 브라우저 환경에서 사용자의 행동을 모방하여, 애플리케이션의 전체 흐름을 테스트하는 것임
-> 실제 비즈니스 로직을 충족하는지 , 요구사항을 충족하는지
사용자 플로우 검증 - 사용자가 애플리케이션을 사용하는 전체 흐름을 테스트 하여 , 모든 기능이 정상적으로 동작하는지를 확인
통합 문제 식별 - 프론트엔드와 백엔드 서비스 간의 통합 문제를 식별하고 해결합니다.
실제 환경 검증 - 실제 브라우저와 환경에서 테스트를 수행하여 , 개발환경과 운영 환경간의 차이로 인한 문제를 발견
E2E 테스트 도구들
Cypress, Playwright, Selenium WebDriver
ex1 )
describe('회원 가입 및 로그인 E2E 테스트', () => {
it('사용자가 회원 가입하고 로그인할 수 있어야 함', () => {
// 회원 가입 페이지로 이동
cy.visit('http://localhost:3000/signup');
// 회원 가입 폼 입력 및 제출
cy.get('input[name="username"]').type('testuser');
cy.get('input[name="email"]').type('testuser@example.com');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
// 회원 가입 성공 메시지 확인
cy.contains('회원 가입이 완료되었습니다').should('be.visible');
// 로그인 페이지로 이동
cy.visit('http://localhost:3000/login');
// 로그인 폼 입력 및 제출
cy.get('input[name="email"]').type('testuser@example.com');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
// 로그인 성공 후 대시보드 확인
cy.url().should('include', '/dashboard');
cy.contains('testuser님 환영합니다').should('be.visible');
});
});
ex2)
const { test, expect } = require('@playwright/test');
test('상품 검색부터 구매까지의 E2E 테스트', async ({ page }) => {
// 메인 페이지로 이동
await page.goto('http://localhost:3000');
// 상품 검색
await page.fill('input[placeholder="검색"]', '헤드폰');
await page.press('input[placeholder="검색"]', 'Enter');
// 검색 결과에서 상품 선택
await page.click('.product-item:has-text("헤드폰")');
// 상품 상세 페이지에서 '구매하기' 클릭
await page.click('text=구매하기');
// 결제 페이지에서 정보 입력
await page.fill('input[name="cardNumber"]', '1234 5678 9012 3456');
await page.fill('input[name="expiryDate"]', '12/25');
await page.fill('input[name="cvv"]', '123');
// 구매 완료
await page.click('button[type="submit"]');
// 구매 완료 확인
await expect(page).toHaveURL(/.*\/order\/confirmation/);
await expect(page.locator('text=구매가 완료되었습니다')).toBeVisible();
});
frontend에서 많이 사용하는 test 패턴 : AAA 패턴
AAA Pattern
A . Arrange : 테스트 환경과 값을 정의
A . Act : 테스트할 내용을 실행
A . Assert : 실행 결과값을 평가 / 예상되어야 하는 결과 혹은 값에 부합하는지를 비교