본문 바로가기

TDD, CleanCode

React 테스트 (Jest, Enzyme)

Jest

자바스크립트 테스트 도구이다.

 

다음과 같이 설치하고

npm install --save-dev jest

npm script를 통해 실행하자.

{
  "scripts": {
    "test": "jest"
  }
}

 

expect(테스트 데이터).toBe(결과 값)

제일 기본이 되는 문법이다.

expect의 파라미터에 테스트할 데이터, toBe에 결과값을 각각 입력한다.

 

1
2
3
4
5
6
7
8
9
10
const sum = require("./sum")
 
test("1+2는 3입니다.", () => {
  expect(sum(12)).toBe(3)
})
 
test("2+2는 4입니다.", () => {
  expect(2 + 2).toBe(4)
})
 

.toEqual(주로 객체 데이터)

객체를 비교 할 때 사용된다.

1
2
3
4
5
test("객체를 비교합니다", () => {
  const data = { one: 1 }
  data["two"= 2
  expect(data).toEqual({ one: 1, two: 2 })
})
 

.not.

부정을 의미한다.

1
2
3
4
5
6
7
test("Not 문을 사용합니다", () => {
  for (let a = 1; a < 10; a++) {
    for (let b = 1; b < 10; b++) {
      expect(a + b).not.toBe(0)
    }
  }
})
 

0과 Null 값에 대한 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test("null에 대한 다양한 부정 테스트입니다.", () => {
  const n = null
  expect(n).toBeNull()
  expect(n).toBeDefined()
  expect(n).not.toBeUndefined()
  expect(n).not.toBeTruthy()
  expect(n).toBeFalsy()
})
 
test("0에 대한 다양한 부정 테스트입니다.", () => {
  const z = 0
  expect(z).not.toBeNull()
  expect(z).toBeDefined()
  expect(z).not.toBeUndefined()
  expect(z).not.toBeTruthy()
  expect(z).toBeFalsy()
})
 
 

숫자 비교 테스트

값이 이상인지 이하인지 같은지에 대한 비교를 할 수 있따.

1
2
3
4
5
6
7
8
9
10
11
test("숫자에 대한 테스트입니다.", () => {
  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)
})
 

실수형 테스트

1
2
3
4
test("float 경우 toBe가 동작하지 않으므로 다음과 같이 사용합니다.", () => {
  const value = 0.1 + 0.2
  expect(value).toBeCloseTo(0.3)
})
 

.toMatch()

정규 표현식을 이용한 문자열 테스트를 한다.

1
2
3
4
5
6
7
8
test("정규표현식을 이용한 문자열 매칭입니다.1", () => {
  expect("team").not.toMatch(/I/)
})
 
test("정규표현식을 이용한 문자열 매칭입니다.2", () => {
  expect("Christoph").toMatch(/stop/)
})
 
 

.toContain()

배열안의 인자가 포함되어 있는지의 여부를 테스트 한다.

1
2
3
4
5
6
7
const shoppingList = ["diapers""kleenex""trash bags""paper towels""beer"]
 
test("배열에 contain 여부에 대한 테스트 입니다.", () => {
  expect(shoppingList).toContain("beer")
  expect(new Set(shoppingList)).toContain("beer")
})
 
 

.toThrow( (Error) )

에러를 테스트하는방법이다.

.toThrow의 에러 인자를 넣어 줄 경우 해당 에러인지 판별한다.

문자열을 넣어준다면 출력되는 문자열을 비교 할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function compileAndroidCode() {
  throw new Error("you are using the wrong JDK")
}
test("예외에 대한 다양한 테스트 방법입니다.", () => {
  //에러 발생을 판단.
  expect(compileAndroidCode).toThrow()
  expect(compileAndroidCode).toThrow(Error)
 
  // 메세지를 정규표현식으로 판단.
  expect(compileAndroidCode).toThrow("you are using the wrong JDK")
  expect(compileAndroidCode).toThrow(/JDK/)
})
 
 

콜백 및 비동기 테스트

콜백 함수의 경우에

fetchData가 다 실행되기 전에 test가 끝난다.

done()을 이용하게 되면 test 안이 모두 수행될때까지 기다리고 마지막에 done을 통해 테스트가 종료되는 식으로 테스트를 할 수 있다.

 

비동기의 경우에는

그냥 async await을 통해 테스트 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function fetchData(func) {
  func("peanut butter")
}
 
test("the data is peanut butter", done => {
  function callback(data) {
    expect(data).toBe("peanut butter")
    done() // fetchData가 동작하기 전에 test가 끝나버린다.
  }
  fetchData(callback)
})
//Promise
 
function fetchPromiseData() {
  return new Promise((res, rej) => {
    res("peanut butter")
  })
}
 
test("the data is peanut butter", async () => {
  const data = await fetchPromiseData()
  expect(data).toBe("peanut butter")
})
 

라이프 사이클

테스트 전 후에 반복되는 코드를 세팅을 할 수 있다.

반복해서 사용하는 객체 등에 사용할 수 있다.

Each는 매번 테스트 마다

All 은 단 한번만 실행 된다.

콘솔이 어떻게 찍히는 지 확인 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeEach(() => {
  console.log("Let's test")
})
 
afterEach(() => {
  console.log("end Test")
})
 
beforeAll(() => {
  return console.log("before only one")
})
 
afterAll(() => {
  return console.log("after only one")
})
 
 

React with Jest

리액트 컴포넌트에 적용해보자.

프로젝트를 커스텀 해볼수도 있지만, 지금은 CRA를 이용하여 프로젝트를 생성하자.

create-react-app react-test

CRA로 설치하면 이미 test 코드가 준비되어 있다.

 

또한 react-scripts가 jest를 내장하고 있을 뿐만 아니라, hot-modules처럼 변화를 감지하고 계속 테스트를 해주는 끝내주는 모듈을 마련해두었다.

 

개인적으로 공부 할 때는 이것저것 이미 만들어져있는것을 하는 것을 좋아하진 않지만...

일단 Jest를 익히는게 우선이니까 다음에 커스텀 해보도록 하자.

 

npm run test를 하면 

다음과 같은 메뉴가 떠야 정상이다.

어려운 영어는 아니니까 읽어보고 사용하면 좋을 거 같다.

a를 눌러 실행해보자.

컴포넌트 테스트 (Snapshot)

컴포넌트를 테스트 하기 위해서 예상되는 값에 컴포넌트를 입력하는 것은 힘들다.

이 때문에 Snapshot이라는 개념이 생겨 난거 같은데, 컴포넌트를 문자열 형태로 전환 하고 이를 __snapshot__파일에 저장한다. 만일 스냅샷에 컴포넌트가 이미 있다면 덮어 쓰지 않는다.

이후에 실제 생성될 컴포넌트를 이 스냅샷과 비교한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import renderer from "react-test-renderer"
import App from "./App"
 
describe("App", () => {
  let component = null
 
  it("renders correctly", () => {
    component = renderer.create(<App />)
  })
 
  it("matches snapshot", () => {
    const tree = component.toJSON()
    expect(tree).toMatchSnapshot()
  })
})
 
 

이 때 추가적으로 react-test-renderer 패키지를 설치해주어야 한다.

react-test-render를 통해 컴포넌트를 테스트 할 수 있는 형태로 만들고, toJSON을 이용하여 이를 JSON형태로 만든다. .toMatchSnapshot() 을 통해 만약 스냅샷이 없다면 스냅샷을 만들고 있다면 이미 만들어진 스냅샷과 비교할 값을 비교한다.

State 테스트

state 테스트를 위한 타이머를 하나 만들자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from "react"
 
class Counter extends Component {
  state = {
    value: 1
  }
  onIncrease = () => {
    this.setState(({ value }) => ({ value: value + 1 }))
  }
  onDecrease = () => {
    this.setState(({ value }) => ({ value: value - 1 }))
  }
  render() {
    const { value } = this.state
    const { onIncrease, onDecrease } = this
    return (
      <div>
        <h1>Counter</h1>
        <h2>{value}</h2>
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    )
  }
}
 
export default Counter
 
 

< Timer.js >

 

버튼을 누를 시에 제대로 state가 변화하는지 체크 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from "react"
import renderer from "react-test-renderer"
import Counter from "./Counter"
 
describe("Counter 테스트", () => {
  let component = null
 
  it("렌더링 테스트", () => {
    component = renderer.create(<Counter />)
  })
 
  it("스냅샷과 비교", () => {
    const tree = component.toJSON()
    expect(tree).toMatchSnapshot()
  })
 
  it("더하기 기능", () => {
    component.getInstance().onIncrease()
    expect(component.getInstance().state.value).toBe(2)
    const tree = component.toJSON() // re-render
    expect(tree).toMatchSnapshot() // 스냅샷 비교
  })
 
  it("빼기 기능", () => {
    component.getInstance().onDecrease()
    expect(component.getInstance().state.value).toBe(1// value 값이 1인지 확인
    const tree = component.toJSON() // re-render
    expect(tree).toMatchSnapshot() // 스냅샷 비교
  })
})
 
 

< Timer.test.js >

.getInstance() 를 이용하면 해당 컴포넌트의 대부분의 정보가 나온다.

그중 state는 물론이고 onIncrease()와 같은 메소드도 존재한다.

이를 각각 실행시켜서 비교해 볼 수 있다.

 

DOM 시뮬레이션 with Enzyme

이번에는 해당 컴포넌트의 버튼을 누르거나 submit을 하는 등의 시뮬레이션을 통해서 테스팅 하는 방법을 알아 볼 것이다.

 

이를 위해서는 react-test-renderer만의 힘으로는 역부족이고 다음과 같은 패키지가 필요하다.

npm i enzyme
npm i enzyme-adapter-react-16

그리고 다음 내용의 파일을 하나 생성한다.

1
2
3
4
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
 
Enzyme.configure({ adapter: new Adapter() });

이 때 CRA로 만들시에는 내부 jest 설정으로 인해 파일명이 무조건 setupTests.js 이다.

나머지의 경우에는 root디렉토리에 jest.setup.js 인듯 하다.

 

테스트를 위한 간단한 폼-리스트 컴포넌트를 만들어 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from "react"
import Counter from "./components/Counter"
import Form from "./components/Form"
import List from "./components/List"
class App extends Component {
  state = {
    names: ["First""Second"]
  }
  onInsert = name => {
    this.setState(({ names }) => ({ names: names.concat(name) }))
  }
  render() {
    const { names } = this.state
    const { onInsert } = this
    return (
      <div>
        <Counter />
        <hr />
        <h1>List</h1>
        <Form onInsert={onInsert} />
        <List names={names} />
      </div>
    )
  }
}
export default App
 
 

< App.js >

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { Component } from "react"
 
class NameForm extends Component {
  state = {
    name""
  }
  onChange = e => {
    this.setState({
      name: e.target.value
    })
  }
  onSubmit = e => {
    const { name } = this.state
    const { onInsert } = this.props
    onInsert(name)
    this.setState({
      name""
    })
    e.preventDefault()
  }
  render() {
    const { onSubmit, onChange } = this
    const { name } = this.state
    return (
      <form onSubmit={onSubmit}>
        <label>Name</label>
        <input type="text" value={name} onChange={onChange} />
        <button type="submit">Submit</button>
      </form>
    )
  }
}
 
export default NameForm
 

< Form.js >

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from "react"
 
class List extends Component {
  static defaultProps = {
    names: []
  }
 
  renderList() {
    const { names } = this.props
    const nameList = names.map((name, i) => <li key={i}>{name}</li>)
    return nameList
  }
 
  render() {
    return <ul>{this.renderList()}</ul>
  }
}
 
export default List
 
 

< List.js >

 

Form에 입력하고 Submit하면 List가 추가되는 형식이다.

Form 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React from "react"
import { shallow } from "enzyme"
import Form from "./Form"
 
describe("NameForm", () => {
  let component = null
  let changed = null
  const onInsert = name => {
    changed = name
  }
 
  it("renders correctly", () => {
    component = shallow(<Form onInsert={onInsert} />)
  })
 
  it("matches snapshot", () => {
    expect(component).toMatchSnapshot()
  })
 
  describe("새로운 텍스트 입력", () => {
    it("폼의 여부", () => {
      expect(component.find("form").exists()).toBe(true)
    })
    it("인풋의 여부", () => {
      expect(component.find("input").exists()).toBe(true)
    })
    it("인풋 변화 시뮬레이션", () => {
      const mockedEvent = {
        target: {
          value: "hello"
        }
      } 
      component.find("input").simulate("change", mockedEvent)
      expect(component.find("input").props().value).toEqual("hello")
    })
    it("출력 체크", () => {
      const mockedEvent = {
        preventDefault: () => null 
      }
      component.find("form").simulate("submit", mockedEvent)
      expect(component.state().name).toBe(""
      expect(changed).toBe("hello")
    })
  })
})
 
 

20 번째 줄부터 살펴보자

find()

컴포넌트의 태그를 선택할 수 있다. 이후 exists() 를 이용하여 태그가 있는지 확인하였다.

.simulate(e,obj)

이벤트를 시뮬레이션 한다.

e는 이벤트의 위치 obj는 e가 사용하는 데이터이다.

쉽게 예시의 경우 e.target.value = "hello"인 경우이다.

.props()

react-test-renderer 의 getInstance() 와 달리 enzyme는 props()를 통해 태그의 속성에 접근한다.

 

마지막으로 42번줄 같은 경우는

state로 input의 내용을 확인했다.

 

 

다음 포스팅으로 Next.js와 Redux의 테스팅에 대해 알아보자.