๐Ÿ“– ToDo List๋ฅผ Jest๋กœ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ-03_Jest test code ๋Œ๋ ค๋ณด์ž

์Š˜ยท2024๋…„ 11์›” 25์ผ

๐Ÿ“– TIL

๋ชฉ๋ก ๋ณด๊ธฐ
4/90

ToDo List vue3

git : https://github.com/sum529-create/vue3-crud

๊ฐ„๋‹จํ•œ CRUD ๊ธฐ๋Šฅ์˜ ToDo-List๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค!
ํ…Œ์ŠคํŠธ ๋ชฉ์ ์ธ์ง€๋ผ UI์ค‘์ ์ ์ด ์•„๋‹ˆ๋ผ.. ๋งˆ์Œ์—” ์•ˆ๋“ค์ง€๋งŒ ์–ด์จŒ๋“ !

์ผ๋‹จ Composable์— ํ›…์œผ๋กœ ๋นผ๋†“์€ useTodo.js๋ฅผ ๋ณด์ž!

src/composable/useTodo.js

import { onMounted, ref } from 'vue';

function useTodo(){
  const itemList = ref([]);
  const selectedItem = ref({});
  
  function fetchList(){
    const result = [];
    for(let i=0; i<localStorage.length; i++){
      const key = localStorage.key(i);
      const todoItem = localStorage.getItem(key);
      
      try {
        const parsedItem = JSON.parse(todoItem);
        result.push(parsedItem); // ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐฐ์—ด์— ์ถ”๊ฐ€
      } catch (error) {
        console.error("Invalid JSON format for key:", key, todoItem);
      }
    }
    itemList.value = result;
  }
  function newItem(item){
    const idx = crypto.randomUUID();
    const newItemAbout = {
      idx: idx,
      content: item,
      completed: false,
    }
    localStorage.setItem(idx, JSON.stringify(newItemAbout));
    itemList.value.push(newItemAbout);
  }
  function removeTodo(item, i){
    itemList.value.splice(i, 1);
    localStorage.removeItem(item.idx);
  }
  function modifyTodo(item){
    // Object.assign(selectedItem.value, item);
    selectedItem.value = {...item};
  }
  function toggleCompleted(item){
    localStorage.setItem(item.idx, JSON.stringify(item));
  }
  function changeTodo(item){
    const index = itemList.value.findIndex(e => e.idx === item.idx);
    if(index !== -1){
      itemList.value[index] = {...item};
      localStorage.setItem(item.idx, JSON.stringify(item))
    }
    
  }
  function onCancelTodo(){
    selectedItem.value = '';
  }
  onMounted(() => {
    fetchList();
  })
  return { newItem, itemList, removeTodo, modifyTodo, selectedItem, toggleCompleted, changeTodo, onCancelTodo }
}

export default useTodo;

ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ƒ์„ฑ

์šฐ์„  ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด๋ณด์ž

useTodo.test.js

src/composable/__tests__/useTodo.test.js๋ผ๋Š” ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ด๋ฆ„์€ ๋ณดํ†ต ํŒŒ์ผ๋ช….test.js ํ˜•์‹์œผ๋กœ ์ง“์Šต๋‹ˆ๋‹ค.

import { nextTick } from 'vue';
import useTodo from '../useTodo';


describe('useTodo composable', () => {
  let todo;

  beforeEach(() => {
    // ์ดˆ๊ธฐํ™”
    localStorage.clear();
    todo = useTodo();
  });

  test('์ดˆ๊ธฐ ๋กœ๋“œ: fetchList()', async () => {
    // Arrange: ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ํ•ญ๋ชฉ ์ถ”๊ฐ€
    const mockItem = { idx: '1', content: 'Test Todo', completed: false };
    localStorage.setItem('1', JSON.stringify(mockItem));

    // Act: fetchList ํ˜ธ์ถœ
    todo.fetchList();

    // Assert: itemList๊ฐ€ ๋กœ๋“œ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    expect(todo.itemList.value).toEqual([mockItem]);
  });

  test('์ƒˆ๋กœ์šด ํ•  ์ผ ์ถ”๊ฐ€: newItem()', async () => {
    // Act: ์ƒˆ๋กœ์šด ํ•  ์ผ์„ ์ถ”๊ฐ€
    todo.newItem('New Todo');

    // Assert: itemList์— ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    expect(todo.itemList.value.length).toBe(1);
    expect(todo.itemList.value[0].content).toBe('New Todo');
  });

  test('ํ•  ์ผ ์‚ญ์ œ: removeTodo()', () => {
    // Arrange: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    const mockItem = { idx: '1', content: 'Delete Me', completed: false };
    todo.itemList.value.push(mockItem);
    localStorage.setItem('1', JSON.stringify(mockItem));

    // Act: ์‚ญ์ œ ์ˆ˜ํ–‰
    todo.removeTodo(mockItem, 0);

    // Assert: itemList์™€ localStorage์—์„œ ์‚ญ์ œ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    expect(todo.itemList.value).toEqual([]);
    expect(localStorage.getItem('1')).toBeNull();
  });

  test('ํ•  ์ผ ์™„๋ฃŒ ํ† ๊ธ€: toggleCompleted()', () => {
    // Arrange: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    const mockItem = { idx: '1', content: 'Toggle Me', completed: false };
    todo.itemList.value.push(mockItem);
    localStorage.setItem('1', JSON.stringify(mockItem));

    // Act: ์™„๋ฃŒ ์ƒํƒœ ํ† ๊ธ€
    mockItem.completed = true;
    todo.toggleCompleted(mockItem);

    // Assert: localStorage์— ์—…๋ฐ์ดํŠธ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    const updatedItem = JSON.parse(localStorage.getItem('1'));
    expect(updatedItem.completed).toBe(true);
  });

  test('ํ•  ์ผ ์ˆ˜์ •: changeTodo()', () => {
    // Arrange: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
    const mockItem = { idx: '1', content: 'Old Content', completed: false };
    todo.itemList.value.push(mockItem);
    localStorage.setItem('1', JSON.stringify(mockItem));

    const updatedItem = { ...mockItem, content: 'Updated Content' };

    // Act: ์ˆ˜์ • ์ˆ˜ํ–‰
    todo.changeTodo(updatedItem);

    // Assert: itemList์™€ localStorage๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    expect(todo.itemList.value[0].content).toBe('Updated Content');
    const storedItem = JSON.parse(localStorage.getItem('1'));
    expect(storedItem.content).toBe('Updated Content');
  });

  test('์„ ํƒ ์ทจ์†Œ: onCancelTodo()', () => {
    // Arrange: ์„ ํƒ๋œ ํ•ญ๋ชฉ ์ถ”๊ฐ€
    const mockItem = { idx: '1', content: 'Selected', completed: false };
    todo.selectedItem.value = mockItem;

    // Act: ์„ ํƒ ์ทจ์†Œ
    todo.onCancelTodo();

    // Assert: selectedItem์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
    expect(todo.selectedItem.value).toBe('');
  });
});

jest ๋ฌธ๋ฒ•์œผ๋กœ ์ž‘์„ฑ์„ ์™„๋ฃŒ ํ›„!

npm run test

ํ…Œ์ŠคํŠธ ์‹คํ–‰!

์•—..! localStorage is not defined" ์—๋Ÿฌ๋ฐœ์ƒ..


๋ฌธ์ œ์˜ ์›์ธ

ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ์ด์œ ๋Š” Jest๊ฐ€ ์‹คํ–‰๋˜๋Š” ํ™˜๊ฒฝ์—์„œ localStorage๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
localStorage๋Š” ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, Jest๋Š” Node.js ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰๋˜๋ฏ€๋กœ localStorage๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

Jest ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋„ localStorage์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” Mock(๊ฐ€์งœ ๊ตฌํ˜„)์„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


์ด์ œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋‹จ๊ณ„๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ์ง„ํ–‰ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

1. jest.setup.js ํŒŒ์ผ์ƒ์„ฑ ํ›„ localStorage Mock์„ ์ถ”๊ฐ€

global.localStorage = {
  store: {},
  getItem(key) {
    return this.store[key] || null;
  },
  setItem(key, value) {
    this.store[key] = String(value);
  },
  removeItem(key) {
    delete this.store[key];
  },
  clear() {
    this.store = {};
  },
};

1-1. jest-preset.js์— ์ถ”๊ฐ€

module.exports = {
  preset: '@vue/vue3-jest',
  transform: {
    '^.+\\.vue$': '@vue/vue3-jest',
    '^.+\\.js$': 'babel-jest',
  },
  testEnvironment: 'jest-environment-jsdom',  // ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •
  setupFilesAfterEnv: ['./jest.setup.js'],
};

2. Mock์ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ

.... ์—ฌ์ „ํžˆ ๋ฐœ์ƒ. ์•„๋งˆ๋„ Jest๊ฐ€ ์ด ์„ค์ •์„ ์ œ๋Œ€๋กœ ์ฝ์ง€ ๋ชปํ•˜๊ฑฐ๋‚˜, localStorage๊ฐ€ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์— ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋“ฏ ํ•ฉ๋‹ˆ๋‹ค..


3. useTodo.test.js ์— ์ง์ ‘ ์ถ”๊ฐ€

jest.setup.js์— ์ถ”๊ฐ€๋œ localStorage Mock์ด ์ž˜ ์ ์šฉ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, ํ…Œ์ŠคํŠธ ํŒŒ์ผ์— ์ง์ ‘ Mock์„ ์ถ”๊ฐ€ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
์œ„ ์ฝ”๋“œ๋ฅผ ์ƒ๋‹จ๋ถ€์— ์ง์ ‘ ์ถ”๊ฐ€!

npm run test

์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€๋งŒ.. fetchList()์™€ crypto๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ..!

3-1 TypeError: todo.fetchList is not a function

useTodo.js

function useTodo(){
  	...
	return { newItem, itemList, removeTodo, modifyTodo, selectedItem, toggleCompleted, changeTodo, onCancelTodo, fetchList } 
}
// fetchList๋ฅผ ๋„ฃ์ง€์•Š์•„ ์—๋Ÿฌ!.. ๋‚ด๋ถ€์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š”๊ฑฐ๋ผ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ์ƒ๊ฐํ–ˆ๋Š”๋ฐ.. ํ…Œ์ŠคํŠธ์‹œ์— ํ•„์š”ํ•˜๋‹ค๋Š”๊ฒƒ์„ ๊ฐ„๊ณผ!

3-2 TypeError: crypto.randomUUID is not a function

crypto.randomUUID๋Š” Node.js๋‚˜ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ตœ์‹  API ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. Jest ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์ธ jest-environment-jsdom์€ ์ด API๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.


*์Œ.. ๊ทธ๋ƒฅ ๋‹ค๋ฅธ๊ฒƒ์„ ์จ๋ณผ๊นŒ?*

UUID ์ƒ์„ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ

1) uuid๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜:

npm install uuid

2) useTodo.js์—์„œ uuid์‚ฌ์šฉ:

import { v4 as uuidv4 } from 'uuid';

...
function useTodo() {
  function newItem(item) {
    const idx = uuidv4(); // crypto.randomUUID ๋Œ€์‹  ์‚ฌ์šฉ
...

์–์–!! ์„ฑ๊ณต! ๐Ÿ‘

profile
์ฃผ๋‹ˆ์–ด ํ”„๋ก ํŠธ์—”๋“œ ์„ฑ์žฅ๊ธฐ ๊ธฐ๋ก๊ธฐ๋ก

0๊ฐœ์˜ ๋Œ“๊ธ€