테스트 코드 작성법 정리

뱀기·2022년 10월 14일
0

리팩토링

목록 보기
3/8

1. Using Matchers

Common Matchers

가장 간단한 방법 정확하게 맞는 경우를 찾을때.

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

// object값을 비교할때
test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});
//반대의 경우 .not.toBe
test('adding positive numbers is not zero', () => {
  for (let a = 1; a < 10; a++) {
    for (let b = 1; b < 10; b++) {
      expect(a + b).not.toBe(0);
    }
  }
});

Truthiness

undefined, null and false를 구분하고 싶을때, 혹은 그러지않을때 쓸 수 있다.

- toBeNull matches only null
- toBeUndefined matches only undefined
- toBeDefined is the opposite of toBeUndefined
- toBeTruthy matches anything that an if statement treats as true
- toBeFalsy matches anything that an if statement treats as false
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('zero', () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

Numbers

숫자를 비교함.

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           This won't work because of rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});

Strings

문자열 비교

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

Arrays and iterables

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('the shopping list has milk on it', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

Exceptions

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // You can also use a string that must be contained in the error message or a regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);

  // Or you can match an exact error mesage using a regexp like below
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});

더 많은 메소드는 공식 문서에서 확인가능
https://jestjs.io/docs/expect

2. Testing Asynchronous Code

Promises

// 반드시 return이 필요하다.
test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

// 이렇게도 쓸 수 있겠다!
return expect(fetchData()).resolves.toBe('peanut butter');
// error가 발생해서 'error'를 리턴한다고 했을때
return expect(fetchData()).rejects.toBe('error');

Async/Await

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

Callbacks

If done() is never called, the test will fail (with timeout error), which is what you want to happen.

// done을 반드시 사용해야한다.
test('the data is peanut butter', done => {
  function callback(error, data) {
    if (error) {
      done(error);
      return;
    }
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }

  fetchData(callback);
});

3. 테스트 전후 작업

beforeEach, afterEach

테스트 코드를 여러개 작성하면 값이 계속 누적되어 원하는대로 테스트가 안될 경우 사용할 수 있다.

let num = 10;
// test코드가 실행되기 전에 num을 0으로 만들어주고 코드가 실행됨
beforeEach(() => {
	num = 0
})

// test코드가 실행된 후에 num을 0으로 만들어주고 코드가 실행됨
afterEach(() => {
	num = 0
})

beforeAll, afterAll

전체 테스트 코드가 실행되기 전후에 실행됨. describe안에 각각 동작함.

let user;
// 모든 코드가 실행되기 전에 한번 유저 정보를 받는다.
beforeAll(() => {
	user = getUserDb()
})

// 모든 테스트 코드가 실행되고 유저 정보를 지운다.
afterAll(() => {
	return deleteUserDb()
})

test.only

특정 테스트 코드만 실행.

test.only("0 + 5 = 5", () => {
	expect(add(0, 5)).toBe(5);
});

4. Mock Functions

mocking은 단위 테스트를 작성 시, 해당 코드가 의존하는 부분을 가짜(mock)로 대체하는 기법을 말한다. 일반적으로 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 어려운 경우 또는 부담스러운 경우에 mocking을 많이 사용한다.

// forEach 함수를 테스트해보려고한다.
function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

// 이럴때 fn()을 사용해서 mock function을 만들어 쓸 수 있음.
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);

// .mock property
// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1);

// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);

// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

MockReturn Values

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

Mocking modules

실제 api hitting없이 fake response를 리턴하고 테스트

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));
});

toMatchSnapshot

expect().toMatchSnapshot();

react-testing-library

  1. 정적 컴포넌트 테스팅
// Home.tsx
import type { NextPage } from "next";
import Head from "next/head";
import Image from "next/image";
import LoginForm from "../components/LoginForm";

import styles from "../styles/Home.module.css";

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <LoginForm onSubmit={() => console.log("submit")} />
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  );
};

export default Home;


// home.test.tsx
import { render, screen } from "@testing-library/react";
import Home from "../pages/index";
import "@testing-library/jest-dom";

describe("Home", () => {
  it("Home에 header가 렌더 됐는지 확인한다.", () => {
    render(<Home />);

    const heading = screen.getByRole("heading", {
      name: /welcome to next\.js!/i,
    });

    expect(heading).toBeInTheDocument();
  });
});
  1. 동적 컴포넌트 테스팅
// LoginForm.tsx
import { useState } from "react";

interface Props {
  onSubmit: (email: string, password: string) => void;
}

export default function LoginForm({ onSubmit }: Props) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  return (
    <form onSubmit={() => onSubmit(email, password)}>
      <label>
        email
        <input
          type="email"
          placeholder="user@test.com"
          value={email}
          onChange={({ target: { value } }) => setEmail(value)}
        />
      </label>
      <label>
        password
        <input
          type="password"
          value={password}
          onChange={({ target: { value } }) => setPassword(value)}
        />
      </label>
      <button disabled={!email || !password}>login</button>
    </form>
  );
}

// LoginForm.test.tsx
import React from "react";
import "@testing-library/jest-dom";
import { render, fireEvent } from "@testing-library/react";
import LoginForm from "../../components/LoginForm";

describe("<LoginForm />", () => {
  it("이메일과 패스워드를 전부 입력하면 버튼이 활성화된다.", () => {
    const { getByText, getByLabelText } = render(
      <LoginForm onSubmit={() => null} />
    );

    const button = getByText("login");
    const email = getByLabelText("email");
    const password = getByLabelText("password");

    expect(button).toBeDisabled();

    fireEvent.change(email, { target: { value: "user@test.com" } });
    fireEvent.change(password, { target: { value: "Test1234" } });

    expect(button).toBeEnabled();
  });

  it("버튼을 누르면 submit이 실행된다", () => {
    const onSubmit = jest.fn();
    const { getByText, getByLabelText } = render(
      <LoginForm onSubmit={onSubmit} />
    );
    const button = getByText("login");
    const email = getByLabelText("email");
    const password = getByLabelText("password");

    fireEvent.change(email, { target: { value: "user@test.com" } });
    fireEvent.change(password, { target: { value: "Test1234" } });

    fireEvent.click(button);

    expect(onSubmit).toHaveBeenCalledTimes(1);
  });
});

더 많은 테스팅 라이브러리 정보는 공식 문서를 참조하자
https://testing-library.com/docs/react-testing-library/example-intro

참조

https://jestjs.io/docs/getting-started
https://www.daleseo.com/react-testing-library/

0개의 댓글