Hướng dẫn dùng fireevent JavaScript

Using the fireEvent() Method

/* JavaScript Bible, Fourth Edition by Danny Goodman John Wiley & Sons CopyRight 2001 */

fireEvent() Method

This is a paragraph (with a nested SPAN) that receives click events.

Control Panel

Cancel event bubbling.

Related examples in the same category

Một bài viết tổng hợp, sẽ cố gắng đề cập càng nhiều càng tốt các vấn đề có thể gặp khi đụng đến unit test với React.

Tại sao phải test?

Rất hiển nhiên là chúng ta viết test nhằm mục đích hạn chế được càng nhiều lõi càng tốt, đảm bảo những gì chúng ta viết ra chạy đúng như chúng ta mong muốn. Một vài điểm trừ khi chúng ta phải viết test

  1. Là nó tốn thời gian và tương đối khó khăn (dù là lập trình viên kinh nghiệm cũng gặp không ít vất vả khi mới bắt đầu viết test)
  2. Test pass không có nghĩa ứng dụng, function của chúng ta chạy đúng 100%
  3. Cũng đôi khi, test fail, nhưng ứng dụng, function vẫn chạy hoàn toàn bình thường
  4. Trong vài trường hợp đặc biệt, chạy test trong CI có thể tốn tiền

Tuyển lập trình react lương cao up to 20M

Test cái gì?

Test các chức năng, function của ứng dụng, những cái mà user sẽ sử dụng. Nó giúp chúng ta tự tin vỗ ngực, ứng dụng đáp ứng đúng nhu cầu sử dụng

Không test cái gì

Thích quan điểm của Kent C về việc không nên đi quá chi tiết việc hiện thực. Việc mà code nó hiện thực như thế nào chúng ta không quan tâm, user không quan tâm, chúng ta chỉ quan tâm đầu vào-đầu ra của một function.

Các thư viện của người khác viết cũng là thứ không cần thiết phải test, nó là trách nhiệm của người viết thư viện. Nếu không tin thì đừng dùng nó. Còn nếu thật sự có tâm bạn hãy hỗ trợ cho thư viện đó trên github bằng cách bổ sung test cho nó.

Một vài triết lý cá nhân khi test

Nhiều integration test, không dùng snapshot test, vài unit test, vài e-to-e test.

Hãy viết thật nhiều integration test, unit test thì tốt nhưng nó không thật sự là cách mà người dùng sử dụng ứng dụng. Việc test chi tiết code hiện thực ra sao với unit test rất dễ.

Integration test nên dùng mock (dữ liệu giả lập) ít nhất có thể

Không nên test những cái tiểu tiết như tên hàm, tên biến, cách khai báo biến số, hằng số có hợp lý.

Lấy ví dụ, nếu chúng ta test một button và thay đổi tên function xử lý <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>0 từ <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>1 sang <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>2, test sẽ fail nhưng mọi thứ vẫn hoạt động bình thường.

Shallow vs mount

Mount là phần html, css, js thật sự khi chạy, như cách mà browser sẽ thấy, nhưng theo cách giả lập. Nó không có render, paint bất cứ thứ gì lên UI, nhưng làm như thể nó là browser thật sự và chạy code ngầm bên dưới.

Không bỏ thời gian ra để paint ra UI giúp test chạy nhanh hơn. Tuy nhiên nó vẫn chưa nhanh bằng shallow render

Đó là lý do bạn phải <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>3 và <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>4 sau mỗi test, nếu không test này sẽ gây side-effect lên test kia.

Mount/render thường được sử dụng cho integration test và shallow sử dụng cho unit test.

Kiểu shallow render sẽ chỉ render ra một component đang test mà không bao gồm các component con, như vậy để tách biệt việc test trên từng component độc lập.

Lấy ví dụ như component cha, con như sau

import React from 'react' const App = () => { return ( <div> <ChildComponent /> </div> ) } const ChildComponent = () => { return ( <div> <p>Child component</p> </div> ) }

Nếu chúng ta dùng shallow render component <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>5, chúng ta sẽ nhận được DOM như sau, phần <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>6 sẽ không bao gồm bộ “ruột” bên trong

<App> <div> <ChildComponent /> </div> </App>

Với mount, thì chúng ta có

<App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>

react-testing-library là một thư viện khá ổn cho việc viết unit test react, tuy nhiên Enzyme là nền tảng cần nắm chắc, chúng ta sẽ đề cập nó trước

Enzyme

Cài đặt

npm install enzyme enzyme-to-json enzyme-adapter-react-16

Sơ qua những gì chúng ta sẽ import

import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })

3 cái import đầu tiên là cho React và component đang test, sau đó đến phần của Enzyme, <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>7 là để chuyển shallow component của chúng ta ra thành JSON để lưu thành snapshot

Cuối cùng là Adapter để làm việc được với react 16

Thực hiện test chi tiết với Enzyme

Chúng ta sẽ lấy một ví dụ tại sao ko nên test việc hiện thực chi tiết, với một component <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>8 như thế này

import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;

Trong component trên, chúng ta cố tình gõ sai chữ <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>9, ứng dụng sẽ không chạy, nhưng khi chạy test thì vẫn pass

File test

import React from 'react'; import ReactDOM from 'react-dom'; import Counter from '../counter'; import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }) test('increment method increments count', ( const wrapper = mount(<Counter />); expect(wrapper.instance().state.count).toBe(0) wrapper.instance().increment(); expect(wrapper.instance().state.count).toBe(1) ) => {})

Thứ nhất là cách viết test như vậy có vấn đề, chúng không mô phỏng cách mà user sẽ sử dụng, chúng ta gọi thẳng npm install enzyme enzyme-to-json enzyme-adapter-react-160.

Nếu bạn simulate việc click nút button npm install enzyme enzyme-to-json enzyme-adapter-react-161 thay vì gọi npm install enzyme enzyme-to-json enzyme-adapter-react-160, test sẻ pass, nhưng lỡ đâu, một lần cập nhập nào đó bạn thay đổi npm install enzyme enzyme-to-json enzyme-adapter-react-163 cho button, mà ko cập nhập lại test thì cũng toang.

Vậy người nông dân biết phải làm sao?

React-testing-library

Từ thư viện npm install enzyme enzyme-to-json enzyme-adapter-react-164, nó đưa ra một nguyên lý chung như sau

Test càng gần với thực tế sử dụng của ứng dụng, test càng đem đến sự tự tin cho chúng ta

Hãy tâm niệm nguyên lý này trong đầu, chúng ta sẽ còn bàn tiếp về nó

useState

Hay bắt đầu test React hook, chúng ta đã và đang sử dụng nó nhiều hơn là class component

import React, { useState } from 'react'; const TestHook = (props) => { const [state, setState] = useState("Initial State") const changeState = () => { setState("Initial State Changed") } const changeNameToSteve = () => { props.changeName() } return ( <div> <button onClick={changeState}> State Change Button </button> <p>{state}</p> <button onClick={changeNameToSteve}> Change Name </button> <p>{props.name}</p> </div> ) } export default TestHook;

Prop sẽ được nhận từ component cha là <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>5

const App = () => { const [state, setState] = useState("Some Text") const [name, setName] = useState("Moe") ... const changeName = () => { setName("Steve") } return ( <div className="App"> <Basic /> <h1> Counter </h1> <Counter /> <h1> Basic Hook useState </h1> <TestHook name={name} changeName={changeName}/> ...

Với nguyên lý như đã nói, chúng ta sẽ thực hiện test như thế nào

Cách mà user sử dụng ứng dụng sẽ là: họ thấy một đoạn text trên UI Button, click vào, rồi thấy một kết quả sau khi click đó, một text mới xuất hiện chẳng hạn

Chúng ta cài đặt thư viện npm install enzyme enzyme-to-json enzyme-adapter-react-166 (không phải npm install enzyme enzyme-to-json enzyme-adapter-react-164 nhé)

npm install @testing-library/react

Thực hiện việc test

<App> <div> <ChildComponent /> </div> </App>0

Vì không sử dụng shallow render, nên chúng ta phải gọi npm install enzyme enzyme-to-json enzyme-adapter-react-168 để dọn dẹp sau mỗi lực thực hiện test

npm install enzyme enzyme-to-json enzyme-adapter-react-169 là phương thức nằm trong hàm import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })0, còn vài kiểu query khác nữa, nhưng đây là kiểu mà chúng ta dùng nó nhiều nhất, có thể nói là đủ dùng.

Để test giá trị của state, chúng ta không sử dụng bất cứ tên hàm, tên biến state nào cả. Vẫn là nguyên lý “Không đi sâu vào việc thực hiện chi tiết”. Vì user sẽ thấy một đoạn text trên UI, chúng ta query nó trên DOM, chúng ta cũng query button bằng cách này và bắn ra sự kiện (import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })1). Cuối cùng chúng ta kiểm tra kết quả cuối cùng nhận được, đoạn text bị thay đổi, chứ ko kiểm tra giá trị state (mặc dù nó là tương đương)

useReducer

Reducer chúng ta sẽ test

<App> <div> <ChildComponent /> </div> </App>1

Action

<App> <div> <ChildComponent /> </div> </App>2

Cuối cùng là component sử dụng action và reducer đã định nghĩa

<App> <div> <ChildComponent /> </div> </App>3

Component này sẽ đổi giá trị của import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })2 từ import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })3 sang import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })4 bằng việc dispatch một import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })5 action

Thực hiện test

<App> <div> <ChildComponent /> </div> </App>4

Trước tiên chúng ta test cái reducer bên trong khối import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })6, thực hiện một test đơn giản với giá trị initial state và sau khi có action success.

Với ví dụ trên, reducer và action rất chi là đơn giản, bạn có thể nói không cần thực hiện unit test cho nó làm gì, nhưng trong thực tế sử dụng reducer sẽ không hề đơn giản thế, và việc test reducer là thực sự cần thiết, không những vậy, chúng ta còn phải test theo hướng chi tiết hiện thực bên trong.

Tiếp theo chúng ta có một test cho component, chúng ta vẫn sử dụng cách làm trước đó đã đề cập với import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })7, lấy DOM bằng cách query text và kiểm tra giá trị text sau khi có event click.

useContext

Giờ chúng ta đi đến việc test một component con có thể cập nhập context state trong component cha.

Thường thì context sẽ được khởi tạo trong một file riêng

<App> <div> <ChildComponent /> </div> </App>5

Chúng ta sẽ cần một component cha, nắm giữ Context provider. Giá trị truyền vào cho provider sẽ là giá trị import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })8 và hàm import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })9

<App> <div> <ChildComponent /> </div> </App>6

Component con, đây là component chúng ta muốn test

<App> <div> <ChildComponent /> </div> </App>7

Lưu ý: các giá trị của import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })8, khởi tạo, cập nhập điều nằm trong import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;1, chúng ta chỉ truyền giá trị này xuống các component con thông qua context, mọi thứ điều thực hiện ở <App> <div> <ChildComponent> <div> <p> Child components</p> </div> </ChildComponent> </div> </App>5, cái này quan trọng cần nhớ để hiểu lúc test

<App> <div> <ChildComponent /> </div> </App>8

Với context chúng ta cũng không hề thay đổi cách làm như với import React from 'react' import ReactDOM from 'react-dom' import Basic from '../basic_test' import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json' import Adapter from 'enzyme-adapter-react-16' Enzyme.configure({ adapter: new Adapter() })7, vẫn là tìm và đặt expect thông qua kết quả nhận được cuối cùng.

Bên trong render function, chúng ta có include import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;4 và import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;5 để code dễ đọc hơn, chứ thật sự chúng ta không cần chúng. Test sẽ vẫn chạy nếu truyền vào import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;6 bên trong render function

<App> <div> <ChildComponent /> </div> </App>9

Tại sao lại như vậy?

Hãy nghĩ lại một chút về context, tất cả những state của context được handle bên trong import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;1, vì lý do đó, đây là component chính chúng ta test, mặc dù trông thì có vẻ chúng ta test một child component sử dụng import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;8 hook. Chúng ta lại không thực hiện shallow render, mà render luôn các component con, nên dĩ nhiên import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;9 và import React from 'react'; import ReactDOM from 'react-dom'; import Counter from '../counter'; import Enzyme, { shallow, render, mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }) test('increment method increments count', ( const wrapper = mount(<Counter />); expect(wrapper.instance().state.count).toBe(0) wrapper.instance().increment(); expect(wrapper.instance().state.count).toBe(1) ) => {})0 đều được render vì nó là con của import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({count: this.state.count + 1}) } // đoạn code này mặc dù ko đúng, nhưng khi test vẫn cho kết quả pass // <button onClick={this.incremen}> // Clicked: {this.state.count} // </button> render() { return ( <div> <button className="counter-button" onClick={this.incremen}> Clicked: {this.state.count} </button> </div> )} } export default Counter;6

Chủ đề