Test ứng dụng React Native với Jest (Phần 1)

Mở đầu

Unit test là bước đầu tiên trong quy trình kiểm thử phần mềm. Hãy xem mô hình dưới đây để thấy được tầm quan trọng của Unit tests. Khi càng tăng test ở unit tests sẽ càng giảm test ở các tầng trên.

Testing Triangle

Trong unit test ta sẽ kiểm tra từng phần nhỏ trong code, đó có thể là các method, function trong class, thậm chí là phần nhỏ hơn trong function.

Hôm nay tôi sẽ giới thiệu với các bạn framework của React Native giúp thực hiện công việc test đó chính là Jest.

1.Configure Jest

Từ phiên bản RN 0.38 trở lên, Jest đã được tích hợp sẵn khi bạn tạo project bằng react-native init, vì thế việc setup này đối với bạn có thể đã có sẵn và ko cần setup thêm nữa. Tham khảo cách thêm cách cài đặt tại đây

Kiểm tra file package.json

{
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "babel-jest": "24.9.0",
    "jest": "24.9.0",
    "react-test-renderer": "16.9.0"
  },
  "jest": {
    "preset": "react-native"
  }
}

2.Snapshot testing📸 là gì?

Với Jest chúng ta có thể dễ dàng test các component khi có sự thay đổi props và state.

Snapshot test là công cụ rất hữu ích trong trường hợp bạn muốn chắc chắn rằng UI không bị thay đổi ngoài ý muốn.
Trong react native , snapshot testing là việc tạo ra 1 file snapshot, sau đó trong những lần test sau, các component sẽ tiếp tục tạo ra các rendered output khác để so sánh với file snapshot ban đầu, nếu có sự thay đổi thì kết quả test sẽ fail.

3.Ứng dụng snapshot test vào project

Sau khi tạo project bằng câu lệnh react-native init ta  được cấu trúc thư mục sau:

Cấu trúc thư mục

Ở đây tôi đã thêm thư mục src dùng để chứa file source trong project. Trong thư mục này, tôi sẽ tạo thêm 1 file Button.js

import React, { Component } from 'react';
import { Text, TouchableOpacity, Linking } from 'react-native';
// 1. Changed to a class based component
class Button extends Component {
    constructor(props) {
        super(props);
    }

    // 2. Custom function called onPress TouchableOpacity
    onPressHandler = () => {
        const { onPress, url } = this.props
        if (url) {
            Linking.openURL(url)
        }
        onPress();
    }

    render() {
        const { buttonStyle, textStyle } = styles;
        const { label } = this.props;

        return (
            <TouchableOpacity onPress={this.onPressHandler} style={newButtonStyle}>
                <Text style={textStyle}>
                    {label}
                </Text>
            </TouchableOpacity>
        );
    }
};

const styles = {
    textStyle: {
        alignSelf: 'center',
        color: '#fff',
        fontSize: 16
    },
    buttonStyle: {
        height: 45,
        justifyContent: 'center',
        backgroundColor: '#38ba7d',
        borderBottomWidth: 6,
        borderBottomColor: '#1e6343',
        borderWidth: 1,
        marginLeft: 10,
        marginRight: 10
    }
};

export default Button;

Kết quả Button sẽ giống như thế này:

Như vậy đã xong phần chuẩn bị, bây giờ chúng ta bắt đầu viết test.

Đầu tiên tạo file Button-test.js trong thư mục _tests_

import 'react-native';
import React from 'react';
import Button from '../src/Button';

// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';

describe('Button', () => {
  describe('Rendering', () => {
      it('should match to snapshot', () => {
        const component = renderer.create(
          <Button label={'Click me!'}/>
        ).toJSON();
        expect(component).toMatchSnapshot()
      });
  });
});

Ở đây cần chú ý đến react-test-renderer.

'react-test-renderer' là một thư viện giúp chúng ta có thể tạo ra một snapshot của DOM tree được render bởi  React DOM or React Native component mà không cần sử dụng browser hoặc jsdom.
Thay vì phải render ra các object thật thì nó render ra các JS object để có thể thực hiện test trực tiếp trên Node.

Cấu trúc hàm test:

describe('Button', () => {
	// test stuff
}

Method describe chứa một hoặc nhiều test liên quan. Mỗi lần viết một test mới hãy bọc nó trong describe block.

it('should match to snapshot', () => {
	// actual test
});

Hàm test được truyền vào callback như một tham số trong hàm it. Ngoài ra ta truyền thêm phần mô tả mong muốn hàm thực hiện.

Để có thể test được chúng ta cần chuẩn bị dữ liệu đầu vào(input), dữ liệu đầu ra(output) và hàm so sánh(function) 2 kết quả có trùng khớp nhau không.

Ở ví dụ trên ta có component đóng vai trò là input data (snapshot của DOM tree).

Hàm expect cho phép chúng ta truy cập vào các phương thức matcher để phát hiện ra những sự sai khác. Ở đây chúng ta sử dụng toMatchSnapshot để đảm bảo rằng nó trùng với snapshot gần đây nhất.

Running test: npm test để xem điều gì xảy ra.

Khi chạy snapshot test lần đầu, Jest sẽ tạo một snapshot file trong thư mục _snapshots_

Khi mở file snapshot sẽ thấy nó convert view của chúng ta sang object để tiện so sánh cũng như test.

Điều gì xảy ra nếu chúng ta thay đổi UI

Thay đổi màu nền button backgroundColor: '#0000FF' trong buttonStyle, và chạy lại test npm test 

Ngay lập tức jest báo kết quả test bị FAIL, như đã trình bày ở trên việc thay đổi code dẫn đến bản snapshot mới khác so với bản cũ. Jest còn chỉ ra chỗ thay đổi, thật sự rất tiện lợi.

Vậy nếu muốn cập nhật bản snapshot mới thì sao. Rất đơn giản sử dụng lệnh sau:

npm test -- -u

Kết quả snapshot đã được update lại. Lời khuyên nên commit lên git mỗi khi update lại snapshot.

Ngoài ra còn có các option khác:

4. Matchers trong Jest

expect(2 + 2).toBe(4);

.toBe() là một matcher, dùng để so sánh kết quả thực hiện với kết quả mong muốn.

.toBe() sử dụng Object.is để test. Khi muốn kiểm tra giá trị của object hãy sử dụng toEqual. Ví dụ:

it('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

Nếu muốn so sánh kết quả trái với mong đợi sử dụng .not.toBe()

it('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);
    }
  }
});

Ngoài ra còn có các matches khác.

Truthiness

  • toBeNull so sánh với giá trị null
  • toBeUndefined so sánh với giá trị undefined
  • toBeDefined là hàm cho kết quả ngược lại toBeUndefined.
  • toBeTruthy so sánh với giá trị true.
  • toBeFalsy so sánh với giá trị false.

Numbers

it('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);

  // cả toBe và toEqual đều có thể sử dụng cho numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

Đối với số thực sử dụng toBeCloseTo thay vì toEqual

it('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

Có thể kiểm tra strings  với regular expressions bằngtoMatch

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

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

Arrays và Iterables

Kiểm tra một giá trị có trong Array hay Iterable không bằng cách toContain

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

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

Exceptions

Để kiểm tra một lỗi có thể xảy ra bạn có thể sử dụng toThrow:

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

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

  // You can also use the exact error message or a regexp
  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
  expect(compileAndroidCode).toThrow(/JDK/);
});

5. Mock Functions

Mock giúp bạn mô phỏng các tính chất và hành vi giống hệt như đối tượng thực nhằm kiểm tra tính đúng đắn của các hoạt động bên trong.

Quay lại với ví dụ button, bây giờ chúng ta sẽ test xem sự kiện button có được kích hoạt không

describe('onPressHandler', () => {
      it('should call onPress', () => {
        const mockOnPress = jest.fn();   // 1. mock function
        const component = renderer.create(<Button 
            label= "test label" 
            onPress={mockOnPress}           // 2. passing in mock function as props
        />)
        const instance = component.getInstance(); // 3. getting an instance of component
        instance.onPressHandler();  // 4. manually triggering onPressHandler()
        // Act
        expect(mockOnPress).toHaveBeenCalled();
      });
    });
});

Chạy lại lệnh npm test để xem kết quả:

Bài viết sau tôi sẽ giới thiệu chi tiết hơn về Mock( mock function, class mock, mock modules, ...)

Tham khảo

https://jestjs.io/docs/en/getting-started

https://callstack.com/blog/testing-react-native-with-the-new-jest-part-1-snapshots-come-into-play/#.12zbnbgwc