(최종 프로젝트 진행중)
할 일
- mooin_cat_dev DB를 새로 만들고 구조 먼저 따오고 데이터 복사하기
- ‘mooin_cat_dev’라는 이름으로 새 DB 만들기
- 서버 켜서 synchronization: true로 외래키 설정이랑 디폴트값 같은 설정 그대로 빈 테이블들 만들기
- mooin_cat에서 테이블들 선택하여 ‘데이터 내보내기’로 mooin_cat_dev에 타겟 지정하여 데이터 복사하기
- share_product 파트의 외래키와 관련해서 에러 메세지가 두 번 떳지만 일단 필요한 데이터는 다 들어옴 확인.
(참고: https://islet4you.tistory.com/entry/dbeaver-table-data-다른-DB로-내보내기)
- users 테이블이 ‘
우편번호→ 도로명 주소’, ‘동네명’, ‘동네 인증 여부’ 컬럼 만들기
- users.module에 위치를 인증할 수 있도록 하는 PATCH api 만들기
- users.address_certified 를 true(=1)로 바꾸는 PATCH api 작성
- ‘동네명’이 같거나, 좌표 사이 거리가 1km 이내라면 ‘동네 인증이 완료되었습니다’ 메세지와 함께 ⇒ 프로트에서 체크 후 PATCH api 호출
- 이 프론트를 어디 페이지에 붙일 것인가? 일단 ‘위치 인증하는 페이지’를 만든다. → 아니다. 그냥 js 함수만 만들어둔다. … → 으아 복잡하다. 그냥 페이지로 가서 버튼을 누르면 동작하게 하자!
- 후보1:
최초 회원가입 시 ‘위치 인증 하러 가시겠습니까?’ 모달과 함께 위치 인증 js 함수를 호출한다.
- 후보2: 아니면 상단바에 위치 미인증 유저일 시 ‘위치인증하기’ 버튼을 보이게 할까? 그럴려면 헤더에 user.address_certified가 들어가야 하는 만큼 모든 페이지가 이 변수를 담아 return 받도록 해야 하는 불편함이 있다. → 그냥 ‘위치 접근 허용을 해주시고 마이페이지 가서 위치 인증 버튼을 눌러주세요!’ 하고 소개하고 끝내야지..! ✔️
- 후보1:
- 이 프론트를 어디 페이지에 붙일 것인가? 일단 ‘위치 인증하는 페이지’를 만든다. → 아니다. 그냥 js 함수만 만들어둔다. … → 으아 복잡하다. 그냥 페이지로 가서 버튼을 누르면 동작하게 하자!
- 회원가입 페이지에 다음 우편 찾기 api 붙여놓기
- 회원가입 dto 바꾸기.
※ 이하는 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※
Jest로 프론트 동작 테스트하기 - 조사중

이곳과
https://github.com/facebook/jest/tree/main/examples/jquery
이곳의 코드를 조사중에 있다. 일단 대강은 이해가 가는데 예제에 사용된 두 파일 displayUser.js와 fetchCurrentUser.js 구조가 이해가지 않아서.
일단 폴더 구조:

// fetchCurrentUser.js
const $ = require('jquery');
function parseJSON(user) {
return {
fullName: `${user.firstName} ${user.lastName}`,
loggedIn: true,
};
}
function fetchCurrentUser(callback) {
return $.ajax({
success: user => callback(parseJSON(user)),
type: 'GET',
url: 'http://example.com/currentUser',
});
}
module.exports = fetchCurrentUser;
⇒ ajax 호출 자체를 반환하면 어떻게 되는 거지? success가 실행된 결과를 반환하는 건가? 아! callback을 실행한 결과를 반환하라는 것 같다. user를 parseJSON 함수에 넣어서 돌려받은 { fullName, loggedIn } 객체를 인수로 넣어서! callback은 호출 시점에 주어질 것이고.
// displayUser.js
const $ = require('jquery');
const fetchCurrentUser = require('./fetchCurrentUser.js');
$('#button').click(() => {
fetchCurrentUser(user => {
const loggedText = `Logged ${user.loggedIn ? 'In' : 'Out'}`;
$('#username').text(`${user.fullName} - ${loggedText}`);
}); // => 이 부분이 바로 ajax 응답 성공시에 실행될 callback!
});
⇒ fetchCurrentUser의 호출 시점은 버튼 클릭이 일어났을 때이다.
⇒ ‘success’시에 사용될 callback이 바로 (user) ⇒ { …. } 부분이 되겠다. 즉,
- 버튼을 클릭함
- fetchCurrentUser에게
(user) => { const loggedText = `Logged ${user.loggedIn ? 'In' : 'Out'}`; $('#username').text(`${user.fullName} - ${loggedText}`); }
라는 콜백 함수를 담아서 호출함.
- ajax 콜이 불리고 GET /currentUser로 http 요청을 보냄
- 성공적으로 응답을 받으면 응답으로 받은 결과값을 parseJSON()에게 담아보내서 결과값을 얻어오고,
- parseJSON(user)의 결과로 { fullName: …, loggedIn: true }같은 객체가 돌아오면 이것이 그 자체로 다시 다음의 콜백
(user) => {
const loggedText = `Logged ${user.loggedIn ? 'In' : 'Out'}`;
$('#username').text(`${user.fullName} - ${loggedText}`);
}
함수에 ‘user’ 인자로써 전달됨. 즉, 이 때의 ‘user’ 는 { fullName: …, loggedIn: true }인 상태. 따라서 user.loggedIn과 user.fullName 호출이 자연스럽다.
⇒ parseJSON(user)에서의 user는 GET /currentUser로 http 요청의 응답 객체이고, callback(user)에서의 user는 parseJSON()에서 반환된 결과 객체라는 점이라는 것이 이해의 핵심!
⇒ 즉 parseJSON(user)에서는 http 응답에서 필요한 user 데이터를 가공해 객체로 넘기고, callback(user)에서 웹 페이지의 DOM element에 내용을 붙이는 역할을 한다.
⇒ 즉 fetchCurrentUser(callback)을 임의로 호출하려면 ‘원하는 콜백’을 만들어 넣어야 한다. 이 때 유효한 callback은 인자 ‘user’를 받는데 이게 parseJSON()을 이미 거쳐온 것 같은 형태, 즉 { fullName: ‘김서방’, loggedIn: true }의 형식이어야 함. 또 이 callback의 리턴 값은 …없다. 그저 DOM 요소에 텍스트를 넣어주는 행동뿐.
이제(야) 테스트 코드를 보면:
// __tests__/displayUser-test.js
'use strict';
// 1. fetchCurrentUser.js에 있는 모든 함수를 mocking함 -> fetchCurrentUser뿐이므로 이것의 mock function을 읽어들임
jest.mock('../fetchCurrentUser');
test('displays a user after a click', () => {
// 2. 가짜 페이지(DOM)를 만든다:
document.body.innerHTML =
'<div>' +
' <span id="username" />' +
' <button id="button" />' +
'</div>';
// (그냥 side-effect를 실행하기 위한 임포트. 이 파일의 전역 변수들이 실행된다.)
// 3. displayUser.js를 읽어들인다. 1) jQuery 변수 임포트, 2) fetchCurrentUser 임포트(mock 버전이 아닌 걸로), 3) '#button' 요소에 클릭 이벤트 등록
require('../displayUser');
// 4. 필요 함수를 임포트해오고
const $ = require('jquery'); // 3에서 했는데 또 해야할까?
const fetchCurrentUser = require('../fetchCurrentUser'); // 위에서 파일 자체를 mocking한 것 때문에 이렇게 임포트해와도 fetchCurrentUser는 auto-mocked 상태가 된다.(=반환값이 undefined임 등등)
// 5. fetchCurrentUser mock 함수의 작동(인풋과 아웃풋)을 정의함.
// fetchCurrentUser()는 callback을 인자로 받고 ajax를 호출하여 JQueryXHR 응답 객체를 반환하는 함수인데, 테스트 시에는 실제로 http 요청을 보내지 않도록 하기 위한 mocking임.
fetchCurrentUser.mockImplementation(cb => {
cb({
fullName: 'Johnny Cash',
loggedIn: true,
});
});
// 6. 버튼 클릭 동작을 시행하도록 한다.
$('#button').click();
// 7. fetchCurrentUser가 한 번은 호출됐는가와 #username DOM 요소에 기대한 결과값이 들어 있는가를 테스트함
expect(fetchCurrentUser).toBeCalled();
expect($('#username').text()).toBe('Johnny Cash - Logged In');
});
⇒ 궁금한 점이, 정작 fetchCurrentUser를 ‘덧씌워 정의’한 것은 저 단순한 결과값밖에 없는데, 어떻게 내부에 버튼 이벤트가 정의되어 있고 텍스트를 DOM 요소에 붙이는 내용까지 남아있을 수 있지?
이것과 이것이 어떻게 같을 수 있는 거지?
// mock
(cb) => {
cb({
fullName: 'Johnny Cash',
loggedIn: true,
});
}
// real
(user) => {
const loggedText = `Logged ${user.loggedIn ? 'In' : 'Out'}`;
$('#username').text(`${user.fullName} - ${loggedText}`);
}
아니다… 나는 사실 fetchCurrentUser 가 정확히 어떤 것을 반환하는지 모르는 상태인 것이다..! $.ajax()의 반환값은 JQueryXHR 타입이다.
잠깐 Jest 공부와 주저리
어쨌든 이것 대로라면 굳이 fetchCurrentUser에서 ajax 결과값을 반환해주지 않아도 될텐데…
아무튼 fetchCurrentUser 자체는 콜백을 인자로 받아서 JQueryXHR를 반환값으로 뱉는 함수이다. 아! 그래서 mock의 경우에 콜백 ‘cb’을 받아서 거기에 { fullName: ‘Johnny Cash’, loggedIn: true }를 인수로 주어 실행시키도록 한 거구나! 리턴값은 굳이 JQueryXHR을 내보내고 있지 않다:
// fetchCurrentUser 함수의 정의를 다시 보면 이해할 수 있다: function fetchCurrentUser(callback) { return $.ajax({ success: user => callback(parseJSON(user)), type: 'GET', url: 'http://example.com/currentUser', }); } // mock (cb) => { cb({ fullName: 'Johnny Cash', loggedIn: true, }); }
⇒ 즉, 저 mock 부분의 의미는 ‘cb라는 콜백함수를 받아서 그걸 { … }를 인수로 받아 실행하도록 한다’ 이다.
그럼 결국 ‘어떤 실행을 할 것이냐’라는 함수 body 부분이 원본 그대로 살아 있어야 동작할 텐데, 뭐가 어떻게되는 거지..
jest.mock()이란
https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options
: 위의 사용법 그대로 ‘모듈’을 auto-mocked 버전으로 mocking해오는 것.
파일에도 적용될 수 있는가. 그렇담 그 내용 그대로를 ‘복사’해오는 것인가.
// banana.js module.exports = () => 'banana'; // __tests__/test.js jest.mock('../banana'); // auto-mock 버전으로 mock. const banana = require('../banana'); // banana will be explicitly mocked. banana(); // will return 'undefined' because the function is auto-mocked.
⇒ jest.mock()으로 모듈을 mock하고 나면, 그 후로 그 모듈 안의 export 함수를 임포트 해와도 알아서 mock 함수가 되는 것인가?
mockFn.mockImplementation이란
: 함수의 내용을 갈아끼우는 것 맞다.
: “Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called.”
참고로,
jest.fn(implementation)
=jest.fn().mockImplementation(implementation)
라고 한다.(참고: https://jestjs.io/docs/mock-function-api/#mockfnmockimplementationfn)
활용 예) Mock Implementations (https://jestjs.io/docs/mock-functions#mock-implementations)
The
mockImplementation
method is useful when you need to define the default implementation of a mock function that is created from another module:// foo.js module.exports = function () { // some implementation; }; // test.js jest.mock('../foo'); // this happens automatically with automocking const foo = require('../foo'); // foo is a mock function foo.mockImplementation(() => 42); foo(); // > 42
Jest에서 ES6 class를 mocking 하는 4가지 방법
(공식 문서: https://jestjs.io/docs/es6-class-mocks#automatic-mock)
- Automatic mock:
Calling
jest.mock('./sound-player')
returns a useful "automatic mock" you can use to spy on calls to the class constructor and all of its methods. It replaces the ES6 class with a mock constructor, and replaces all of its methods with mock functions that always returnundefined
. Method calls are saved intheAutomaticMock.mock.instances[index].methodName.mock.calls
.⇒ ‘auto-mock’이란, 클래스의 생성자와 메소드(속성은 제외!)들을 가짜로 복사해오는 것인데 내용물(실행 바디)는 그대로 두고 리턴값만 전부 undefined로 만들어서 가져오는 것이다. 아니다. 내용물을 전부 비우고 리턴값도 undefined인 mock functions로 바꿔서 가져온다.
Mock functions
Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with
new
, and allowing test-time configuration of return values.⇒ Mock function은 실제 함수의 바디를 지우고, 그 함수를 호출하는 콜을 (호출 파라미터와 함께) 쎄비고, new와 함께 생성된 클래스 인스턴스들을 쎄비고, 나중에 원하는 테스트 결과값을 집어넣어 확인할 수 있게 하는, 말 그대로 ‘가짜’ 함수이다.
- Manual mock
: 예를 들면 sound-player.js에 있는 실제 함수를 __mocks__/sound-player.js같이 아예 테스트용 함수로 __mocks__ 폴더 안에 따로 정의해두는 것.
// 예시: __mocks__/sound-player.js // Import this named export into your test file: export const mockPlaySoundFile = jest.fn(); const mock = jest.fn().mockImplementation(() => { return {playSoundFile: mockPlaySoundFile}; }); export default mock;
- jest.mock() 호출할 때 원하는 함수(factory)도 넣어서 호출하기
:
jest.mock(path, moduleFactory)
이런 식으로 moduleFatory 자리에 mock을 반환하는 어떤 함수를 넣어서 쓸 수도 있다. 주의할 점은 항상 .mock() 내부에 이 moduleFactory를 정의해줘야지, 바깥에 써놓고 호출하면 (호이스팅 관련된 문제로) 에러가 많이 난다고 한다.// 괜찮은 코드: import SoundPlayer from './sound-player'; const mockPlaySoundFile = jest.fn(); jest.mock('./sound-player', () => { return jest.fn().mockImplementation(() => { return {playSoundFile: mockPlaySoundFile}; }); }); // out-of-scope Error가 나는 실패 코드: import SoundPlayer from './sound-player'; const fakePlaySoundFile = jest.fn(); jest.mock('./sound-player', () => { return jest.fn().mockImplementation(() => { return {playSoundFile: fakePlaySoundFile}; }); }); // Reference Error가 나는 실패 코드: import SoundPlayer from './sound-player'; const mockSoundPlayer = jest.fn().mockImplementation(() => { return {playSoundFile: mockPlaySoundFile}; }); // results in a ReferenceError jest.mock('./sound-player', () => { return mockSoundPlayer; });
- 우선 mock해온 후 mockImplementation()으로 반환값 지정해주기
: 위의 모든 기능을 해줄 수 있는 ‘후 처리 세팅’.
// 예시 import SoundPlayer from './sound-player'; import SoundPlayerConsumer from './sound-player-consumer'; jest.mock('./sound-player'); describe('When SoundPlayer throws an error', () => { beforeAll(() => { SoundPlayer.mockImplementation(() => { return { playSoundFile: () => { throw new Error('Test error'); }, }; }); }); it('Should throw an error when calling playSomethingCool', () => { const soundPlayerConsumer = new SoundPlayerConsumer(); expect(() => soundPlayerConsumer.playSomethingCool()).toThrow(); }); });
참고로,
“Calls to jest.mock are hoisted to the top of the code.”
jest.mock(’./sound-player’)같은 호출은 언제나 최상단으로 호이스팅된다고 한다.
참고:
Mock FUnctions API: https://jestjs.io/docs/mock-function-api/#mockfnmockimplementationfn
Manual Mocks (auto mock과 대조하여): https://jestjs.io/docs/manual-mocks#examples
update news - “Disabled Automocking”: https://jestjs.io/blog/2016/09/01/jest-15#disabled-automocking
- Automatic mock:
잠깐 알아본, CommonJs(CJS)와 ES Module(ECMA Script Module, ESM) 차이
옛날 스타일의 CJS: ‘require()’과 ‘module.exports’ 사용. 파일명은
.cjs
새로운 스타일의 MJS: ‘import’와 ‘export’ 사용. 파일명은
.mjs
(유용한 번역글 참고: https://yceffort.kr/2020/08/commonjs-esmodules)
아무튼 결론:
- 원랜 automocking이 디폴트(테스트 파일에 임포트되는 모든 모듈이 자동으로 mocking 됨)였는데 v15 이후로 jest.mock(moduleName)이라고 따로 선언해줘야 그 모듈이 mock되는 것으로 디폴트가 바뀌었다고 한다. (jest.config.js에서 automock 옵션을 true로 특별히 지정해주거나, 테스트 파일 내에서 jest.enableAutomock() 함수를 호출해주면 automock 임포팅이 다시 가능해진다)
⇒ mocking 하기 원하는 모듈을 (import 해오기 전이든 후든) 명시적으로 jest.mock()으로 호출해줘야 함.
// 1. jest.mock('../fetchCurrentUser'); // 다른 것들 임포트 전에 jest.mock(원하는 모듈) 해주기.(=explicit mocking방식)
- 그리고 왜인진 모르겠으나 또 import(혹은 require) 해와야 한다:
// 4. const fetchCurrentUser = require('../fetchCurrentUser');
- 그냥 displayUser.js를 읽어옴으로 인해 DOM의 버튼에 클릭 이벤트가 ‘등록’된 것이었다!
// 3. displayUser.js에서 1), 2), 3)을 실행함. require('../displayUser'); // displayUser.js 'use strict'; const $ = require('jquery'); // 1) const fetchCurrentUser = require('./fetchCurrentUser.js'); // 2) $('#button').click(() => { // 3) fetchCurrentUser(user => { const loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out'); $('#username').text(user.fullName + ' - ' + loggedText); }); });
- ⇒ fetchCurrentUser() 함수 자체는 mocking 한다고 해도, 여기서는 fetchCurrentUser함수를 ‘원하는 인수를 넣어서 호출’하는 코드이므로 이 코드 그대로 (mock되지 않고) 저장되어 있게 되는걸까? 그렇다고밖에는 설명이 안 된다. 위의 테스트 코드 전문을 아무리 살펴봐도 DOM 요소에 텍스트를 집어넣는 mocking 작업은 등록되지 않았는데 그걸 테스트 결과로 기대하고 있으므로.
- ⇒ 그렇다면, 그냥 side-effect 효과를 기대하고 읽어들인 임포트 파일과, 거기서 ‘호출’되는 함수는 mocking 대상이 되는 함수일지라도 세탁되지 않고 그대로 존재하게 된다고 이해해도 되려나…
(참고: https://jestjs.io/docs/tutorial-jquery)
그리고 이건 fetchCurrentUser 자체에 대한 테스트 코드 참고용으로:
// __tests__/fetch_current_user.test.js
jest.mock('jquery');
beforeEach(() => jest.resetModules());
it('calls into $.ajax with the correct params', () => {
const $ = require('jquery');
const fetchCurrentUser = require('../fetchCurrentUser');
// Call into the function we want to test
const dummyCallback = () => {};
fetchCurrentUser(dummyCallback);
// Now make sure that $.ajax was properly called during the previous
// 2 lines
expect($.ajax).toHaveBeenCalledWith({
success: expect.any(Function),
type: 'GET',
url: 'http://example.com/currentUser',
});
});
it('calls the callback when $.ajax requests are finished', () => {
const $ = require('jquery');
const fetchCurrentUser = require('../fetchCurrentUser');
// Create a mock function for our callback
const callback = jest.fn();
fetchCurrentUser(callback);
// Now we emulate the process by which `$.ajax` would execute its own
// callback
$.ajax.mock.calls[0 /*first call*/][0 /*first argument*/].success({
firstName: 'Bobby',
lastName: 'Marley',
});
// And finally we assert that this emulated call by `$.ajax` incurred a
// call back into the mock function we provided as a callback
expect(callback.mock.calls[0 /*first call*/][0 /*first arg*/]).toEqual({
fullName: 'Bobby Marley',
loggedIn: true,
});
});
- it 마다 jquery를 새로 만들어주던데, 그러면 jest.mock 자체는 매번 새로 해줄 필요 없나?
- 여기서는 beforeEach(() ⇒ jest.resetModules()) 했는데 아까는 왜 없어도 됐던 거지? 테스트가 하나뿐이어서 그런가?
Jest로 DOM element 테스트를 하고 싶으면 jest-environment-jsdom 설치는 필수
우선
npm i -D jest-environment-jsdom
으로 설치 후, DOM elements와 상호작용 해야할 때마다 테스트하려는 파일 상단에 @jest-environment
docblock을 추가하기만 하면 사용 가능하다:
/**
* @jest-environment jsdom
*/
test('use jsdom in this test file', () => {
const element = document.createElement('div');
expect(element).not.toBeNull();
});
파일별로 말고 전체 폴더에 적용하고 싶으면 testEnvironment: ‘node’ 대신 ‘jsdom’이라고 설정해준다.
(참고: https://jestjs.io/docs/next/configuration#testenvironment-string)
Uploaded by N2T