4주차는 회사일이 급해서 회사일을 더 많이 했으므로 스킵
2주차에서 jest를 설치하고 jest.config.js 설정을 마쳤다면 이번주에는 jest로 단위테스트를 작성할 수 있는 Mocking을 준비하였다.
2주전에 구현한 Oauth2.0 로그인 단위테스트를 작성하면서 진행하였다. Mocking이 끝나면 앞으로의 기능은 회사에서 그랬던 것처럼 TDD로 구현할 생각이다. (TDD에 대해서 썼던 글)
8/26 추가
TDD로 하겠다고 호언장담했었으나 안했었다...😭
1. Axios Mocking
먼저 Axios Mocking이다. jest Mocking Modules 가이드에서 소개하듯이 jest.mock 으로 mocking하는 방법이 있다.
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
그런데 내가 의도하는 건 각 요청 경로 별로 응답을 다르게 설정하고 싶다. (회사처럼)
따라서 jest.spyOn 으로 axios.request를 mock하기로 했다. 이미 회사에서 저장소 분리할 때 한번 해보았기 때문에 껌이겠지 하고 시작했는데 다른 점이 존재했다.
const createMockMethod = (instance: AxiosInstance) => {} | |
export interface AxiosInstanceWithMock extends AxiosInstance { | |
mock: () => MockAxiosInstance | |
} | |
const extendWithMock = (instance: AxiosInstance): AxiosInstanceWithMock => { | |
const instanceWithMock = Object.create(instance) | |
instanceWithMock.mock = createMockMethod(instanceWithMock) | |
return instanceWithMock | |
} | |
const createAxiosInstance = () => { | |
const instance = axios.create() | |
instance.interceptors.response.use(AuthInterceptor) | |
return extendWithMock(instance) | |
} | |
export const globalAxiosInstance = createAxiosInstance() | |
export const withAxios = async <T>(request: RequestConfig): Promise<T> => { | |
/** | |
* @requires response 반드시 proxy 통해서 외부 서버와 통신합니다. | |
*/ | |
const response = await globalAxiosInstance.request<T, T>({ | |
...request, | |
baseURL: `${isSSR ? HOST_URL : ''}/api`, | |
}) | |
return response | |
} |
먼저 전역에서 axios.create 으로 인스턴스를 생성하고 mock 메소드를 추가한다.
mock 메소드에서 axios.request 를 mocking한다.
export const createMockAxiosInstance = (target) => ({ | |
target, | |
async request(...args) { | |
return this.target && (await this.target.request(...args)) | |
}, | |
}) | |
const createMockMethod = (instance: AxiosInstance | MockAxiosInstance) => () => { | |
if (Object.prototype.hasOwnProperty.call(instance, 'mockRespondOnce')) { | |
return instance as MockAxiosInstance | |
} | |
const mockInstance: MockAxiosInstance = createMockAxiosInstance(instance as AxiosInstance) as MockAxiosInstance | |
if (typeof jest !== 'undefined') { | |
mockInstance.mockRespondOnce = <T extends unknown>(path: string, data: T) => { | |
jest.spyOn(instance, 'request').mockImplementationOnce((...args): Promise<T> => { | |
const [pathFromArgs] = args | |
if (pathFromArgs === path || pathFromArgs.url === path) { | |
if (data instanceof Error) { | |
return Promise.reject(data) | |
} | |
return Promise.resolve(data) | |
} | |
throw new Error('path is different from pathFromArgs') | |
}) | |
} | |
} | |
return mockInstance | |
} |
이렇게 인스턴스에 mock 메소드를 추가하고 나면 mock 메소드를 호출할 withAxios를 mock할 모듈을 생성한다.
경로 : utils/__mocks__/fetcher/withAxios.ts
import {AxiosInstanceWithMock} from '$utils/fetcher/withAxios' | |
import {RequestConfig} from '$types/request' | |
const actualModule: {globalAxiosInstance: AxiosInstanceWithMock} = jest.requireActual('utils/fetcher/withAxios') | |
const {globalAxiosInstance: defaultAxiosInstance} = actualModule | |
const mockAxiosInstance = defaultAxiosInstance.mock() | |
export default mockAxiosInstance | |
export const withAxios = async <T = unknown>(req: RequestConfig): Promise<T> => { | |
const res = await mockAxiosInstance.request<T>({ | |
...req, | |
}) | |
return res | |
} |
그런데 매뉴얼적으로 mocking하기 위해 utils/__mocks__/ 하단에 withAxios가 위치하던 경로 그대로 추가하면 jest.mock 이 이것을 인식한다고 알고 있었는데 mock이 먹히지 않는 것이다...😭 (참고)
그래서 jest.mock 의 두번째 인자로 fatory 함수를 넘겨 원래 모듈 대신에 __mock__에 위치한 모듈을 사용하도록 명시하였다.
jest.mock('utils/fetcher/withAxios', () => jest.requireActual('utils/__mocks__/fetcher/withAxios'))
👌 참고
jest --automock 으로 테스트를 실행하면 /__mocks__ 내의 모듈을 자동으로 사용한다. 여기서 자동으로 사용한다는 뜻은 명시적으로 jest.mock() 을 하지 않아도 자동으로 mocking된 모듈을 사용한다는 의미이다.
참조 : jest manual-mocks
사용 예시
위에서 mocking한 withAxios를 사용 예시이다.
jest.mock('utils/fetcher/withAxios', () => jest.requireActual('utils/__mocks__/fetcher/withAxios')) | |
const NAVER_LOGIN_API = '/login/naver/authorize' | |
const mockNaverLogin = (data: AuthorizeResponse | Error) => { | |
withAxios.mockRespondOnce<AuthorizeResponse | Error>(NAVER_LOGIN_API, data) | |
} | |
test('"네이버로 로그인" 버튼을 클릭하면 네이버 인증 api를 호출한다.', async () => { | |
window.location.replace = jest.fn() | |
mockNaverLogin({ | |
redirectUrl: 'https://www.naver.com', | |
}) | |
const {findByText} = render(<Login />) | |
let naverLoginButton: Element | null = null | |
const cb: MatcherFunction = (_content, element) => { | |
if (!element) { | |
return false | |
} | |
if (element.tagName === 'BUTTON' && element.textContent === '네이버로 로그인') { | |
naverLoginButton = element | |
return true | |
} | |
return false | |
} | |
await findByText(cb) | |
if (naverLoginButton) { | |
fireEvent.click(naverLoginButton) | |
} | |
await waitFor(() => { | |
expect(window.location.replace).toHaveBeenCalledWith('https://www.naver.com') | |
}) | |
}) |
2. 🚧 디자인 시스템
디자인 담당님들께서 디자인 시스템을 정리해주시고 계신것을 FE에서는 어떻게 적용할 수 있을지 고민되었다.
현재 tailwindCSS로 반응형 웹을 구축하려고 하고 있는데
바로 이 tailwindCSS의 config를 수정하면 이에 맞는 style을 자동으로 생성해주어 일관된 디자인을 적용할 수 있다.
예를 들어, ColorSet을 다음처럼 정의한다고 가정한다.
@tailwind base; | |
@tailwind components; | |
@tailwind utilities; | |
:root { | |
--primary-green-800: #11373e; | |
--primary-green-700: #5e7c75; | |
--primary-green-600: #818d8a; | |
--primary-yellow-800: #11373e; | |
--grey-800: #22211f; | |
--grey-700: #5f5d59; | |
--grey-600: #807c76; | |
--grey-500: #adaaa5; | |
--grey-400: #bbbbbb; | |
--grey-300: #dfdfdf; | |
--grey-200: #efefef; | |
--grey-100: #f8f8f8; | |
--grey-000: #ffffff; | |
--beige-500: #cbc4a8; | |
--beige-400: #ded9c6; | |
--beige-300: #eae6d7; | |
--beige-200: #f6f4e8; | |
} |
module.exports = { | |
purge: [ | |
'./pages/**/*.{js,ts,jsx,tsx}', | |
'./components/**/*.{js,ts,jsx,tsx}', | |
], | |
darkMode: false, // or 'media' or 'class' | |
theme: { | |
extend: {}, | |
colors: { | |
'primary-green': { | |
800: 'var(--primary-green-800)', | |
700: 'var(--primary-green-700)', | |
600: 'var(--primary-green-600)', | |
}, | |
'primary-yellow': { | |
800: 'var(--primary-yellow-800)', | |
}, | |
beige: { | |
500: 'var(--beige-500)', | |
400: 'var(--beige-400)', | |
300: 'var(--beige-300)', | |
200: 'var(--beige-200)', | |
}, | |
grey: { | |
800: 'var(--grey-800)', | |
700: 'var(--grey-700)', | |
600: 'var(--grey-600)', | |
500: 'var(--grey-500)', | |
400: 'var(--grey-400)', | |
300: 'var(--grey-300)', | |
200: 'var(--grey-200)', | |
100: 'var(--grey-100)', | |
'000': 'var(--grey-000)', | |
}, | |
}, | |
}, | |
variants: { | |
extend: {}, | |
}, | |
plugins: [], | |
prefix: 'tw-', | |
} |
config와 index.css를 수정한 후, tailwindcss 커맨드로 static 경로에 빌드하면 /public/assets/styles/index.css 에 index.css가 생성이 된것을 확인할 수 있다.
npx tailwindcss -i src/styles/index.css -o public/assets/styles/index.css

index.css의 내부 코드를 보면

위에서 정의한 ColorSet이 background, gradient, color 등 Color를 적용할 수 있는 속성 클래스 모두에 적용됨을 확인할 수 있다.
위처럼 스타일이 재사용 가능한 유틸리티 클래스로 정의되는 것을 utility-first CSS라고 부른다.
스타일 가이드 외의 UI 컴포넌트 가이드같은 경우, 원래라면 storybook을 활용하여 관리하겠지만 프로젝트 범위가 그정도까지는 아닌거 같아서 (오히려 공수가 더 들 수 있음) /components 하단에 presenter component로만 관리할 예정이다.
'Projects > 넥스터즈 19기' 카테고리의 다른 글
[NCP] Helm 사용하여 Kubernetes 기반으로 서비스 배포하기 (0) | 2021.08.24 |
---|---|
8/20 Nexters 8주차 : Docker 기반 배포 (0) | 2021.08.20 |
7/16 넥스터즈 3주차 (2) : 에러 처리 설계와 SWR (4) | 2021.07.16 |
7/16 넥스터즈 3주차 (1) : 인증 구현 (0) | 2021.07.16 |
7/10 2주차 세션 : 해커톤 (0) | 2021.07.11 |