Skip to main content

Testing React Native Apps

What are we unit testing exactly ?

We're using "unit testing" to refer to tests of functions and plain JavaScript objects, independent of the React Native framework. This means that we aren't testing any components that rely on React Native.
giphy
For example, a unit could be individual methods and functions in classes or really any small pieces of functionality. We mock out dependencies in these tests so that we can test individual methods and functions in isolation.
These test are written using testing frameworks and for this article i will be using Jest, javascript testing framework together with Enzyme and React Native Testing Library.

Setting

Install

If you use React Native CLI installs the Jest testing framework by default. But if you're using Expo we need to install it manually.
yarn add -D enzyme enzyme-adapter-react-16
More:
yarn add react-dom react-native-testing-library
Create new file jestSetup.js in root and add to jest in package.json:
// jestSetup.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({
  adapter: new Adapter()
});
// package.json
"jest": {
  "preset": "react-native",
  "setupFilesAfterEnv": [
    "<rootDir>/jestSetup.js"
  ]
}
Refer to Jest configuration for more info.
setup-done
With this, our setup should be done tada tada tada

Let's get started by writing a test for a hypothetical function that adds two numbers.

First, create a Sum.js file:
'use strict'

function sum(a, b) {
  return a + b;
};

module.exports = sum;
Create a file named Sum.test.js, this will contain our actual test.
const Sum = require('./Sum');

test('adds 1 + 2 to equal 3', () => {
  expect(Sum(1, 2)).toBe(3);
});
Finally, run yarn test and Jest will print this message:
PASS  ./Sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
ohmygod

Testing React Native components

Component tests are tests of individual React Native components apart from where they are used in an application. I will cover rendering elements, testing props, finding nested elements, updating props, and calling event handlers.
I use react-native-testing-library allows you to test React Native components, verifying the component tree they render and allowing you to interact with them.

Hello world testing

I have component Hello.js and enter the following contents:
// Hello.js
import React from 'react';
import { Text } from 'react-native';

const Hello = () => <Text>Hello, world!</Text>;

export default Hello;
Next, then add a Hello.spec.js file in it with the following contents:
// Hello.spec.js
import React from 'react';
import { render } from 'react-native-testing-library';
import Hello from './Hello';

describe('Hello', () => {

  it('renders the correct message', () => {
    const { queryByText } = render(<Hello />);
    expect(queryByText('Hello, world!')).not.toBeNull();
  });

});
Run test with yarn test

Testing Text and Props

Create a file PassingProps.js and enter the following:
import React from 'react';
import {
  View,
  Text
} from 'react-native';

const PassingProps = ({ name, age = 30 }) => (
  <View>
    <Text testID="name">{`Hello, ${name}!`}</Text>
    <Text testID="age">{`Age ${age}`}</Text>
  </View>
);

export default PassingProps;
Let's test that it displays the right message when a name and age is passed in as a prop. If age undefined default age = 30 Create a file PassingProps.spec.js and add the following:
import React from 'react';
import { render } from 'react-native-testing-library';
import PassingProps from './PassingProps';

describe('PassingProps', () => {

  it('displays the passed-in name', () => {
    const { queryByText } = render(
      <PassingProps name="Jinx" />
    );
    
    expect(queryByText('Hello, Jinx!')).not.toBeNull();
    expect(queryByText('Age 30')).not.toBeNull();
  });

  it('displays the passed-in age', () => {
    const { queryByText } = render(
      <PassingProps name="Jinx" age="25" />
    );
    expect(queryByText('Age 25')).not.toBeNull();
  });
  
});
Here's what's going on:
  • render() renders the component to an in-memory representation that doesn't require environment.
  • queryByText() finds a child component that contains the passed-in text.
  • expect() creates a Jest expectation to check a condition.
  • not.toBeNull() checks that the value is not null, which means that an element with that text was found.

Testing TextInput memo

I have form Login.js:
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  Button,
} from 'react-native';

const Login = props => {

  const [username, _setUsername] = useState('');
  const [password, _setPassword] = useState('');
  const [phone, _setPhone] = useState('');

  _onSubmitLogin = () => {
    // Do something...

    const { login } = props;

    if (login) {
      login({ username, password });
    }

  }

  return (
    <View>
      <Text>Login</Text>
      <TextInput
        testID="username"
        placeholder="Username"
        value={username}
        onChangeText={text => _setUsername(text)}
      />
      <TextInput
        testID="password"
        placeholder="Password"
        value={password}
        secureTextEntry={true}
        onChangeText={text => _setPassword(text)}
      />
      <TextInput
        testID="phone"
        placeholder="Phone"
        value={phone}
        keyboardType="numeric"
        maxLength={10}
        onChangeText={text => _setPhone(text.replace(/[^0-9]/g, ''))}
      />
      <Button
        title="Submit"
        testID="btnSubmit"
        onPress={_onSubmitLogin}
      />
    </View>
  );
}

export default Login;
Let's start by simulating entering usernamepasswordphone and pressing the button submit:
import React from 'react';
import { render, fireEvent } from 'react-native-testing-library';
import Login from './Login';

describe('Login', () => {

  describe('change text login', () => {
    it('change text username and password', () => {
      const { getByTestId } = render(<Login />);

      // use fireEvent change value TextInput
      fireEvent.changeText(getByTestId('username'), 'admin');
      fireEvent.changeText(getByTestId('password'), 'admin@123');

      // use toEqual check value TextInput
      expect(getByTestId('username').props.value).toEqual('admin');
      expect(getByTestId('password').props.value).toEqual('admin@123');
    });


    it('change text phone input ', () => {
      const { getByTestId } = render(<Login />);

      fireEvent.changeText(getByTestId('phone'), '0123456789');

      expect(getByTestId('phone').props.value).toEqual('0123456789');
    });


    it('change text is string to phone input ', () => {
      const { getByTestId } = render(<Login />);

      fireEvent.changeText(getByTestId('phone'), 'isstring');

      expect(getByTestId('phone').props.value).toEqual('');
    });

  });



  describe('Submit form login', () => {

    it('on submit login', () => {
      const data = { "password": "123456", "username": "admin@123" }
      const submitHandler = jest.fn();
      const { getByTestId } = render(

        // passing prop to Login component
        <Login login={submitHandler} />

      );

      fireEvent.changeText(getByTestId('username'), 'admin@123');
      fireEvent.changeText(getByTestId('password'), '123456');

      expect(getByTestId('username').props.value).toEqual('admin@123');
      expect(getByTestId('password').props.value).toEqual('123456');

      // use fireEvent.press call Button submit
      fireEvent.press(getByTestId('btnSubmit'));

      // checking ouput data equal input
      expect(submitHandler).toHaveBeenCalledWith(data);
    });

  })

});
  • getByTestId lets us retrieve an element by the testID prop.
  • fireEvent lets us fire an event on an element specifically here we want the changeText event on the text field, and the press event on the button.
  • toEqual('') checking value TextInput when change.
The other thing we want to confirm is that the login action is called. We can do this using a Sinon spy. A spy allows us to inspect whether it has been called, and with what arguments.
// Login.js
...
if (login) {
  login({ username, password });
}
...
// Login.spec.js
...
const submitHandler = jest.fn();
const { getByTestId } = render(
  <Login login={submitHandler} />
);
...

expect(submitHandler).toHaveBeenCalledWith(data);

Testing with Lifecycle Methods hammer

lifecycle
Component that loads some data from a service upon mount and displays it, Lifecycle.js:
import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
} from 'react-native';

const NUMBERS = ['one', 'two'];

const Lifecycle = () => {

  const [numbers, _setNumbers] = useState([]);

  // use HOOK
  useEffect(() => {
    _setNumbers(NUMBERS);
  });

  return (
    <View>
      {
        numbers.map((num, index) => (
          <Text key={index}>{num}</Text>
        ))
      }
    </View>
  );
}

export default Lifecycle;
Here's my test:
import React from 'react';
import { render } from 'react-native-testing-library';
import Lifecycle from './Lifecycle';

describe('Lifecycle', () => {

  it('loads number from useEffect', () => {
    const { queryByText } = render(<Lifecycle />);

    expect(queryByText('one')).not.toBeNull();
    expect(queryByText('two')).not.toBeNull();
  });

});
If useEffect render after render() because the calls to queryByText() return null and the text is not found. This is because the test doesn't wait for the service to return data.
How can we fix this ? bug
...
const { queryByText, debug } = render(<Lifecycle />);
...

return new Promise((resolve, reject) => {
  setTimeout(() => {
    expect(queryByText('one')).not.toBeNull();
    expect(queryByText('two')).not.toBeNull();
    resolve();
  }, 1000);
});
This works, but there are a few downsides:
  • If the request takes too long, the test can fail sometimes.
  • Which slows down your whole test suite.
  • If the remote server goes down, your test will fail.

Basics of snapshot testing racehorse

Lets setup a basic Button component so that we try out snapshot testing it.
import React, { useState } from 'react';
import {
  TouchableOpacity,
  Text,
} from 'react-native';

const Button = ({ label }) => {

  const [disabled, _setDisabled] = useState(false);

  _onSubmit = () => {
    // Do something...

    _setDisabled(true);

  }

  return (
    <TouchableOpacity
      disabled={disabled}
      onPress={_onSubmit}>
      <Text>
        {disabled ? 'Loading...' : label}
      </Text>
    </TouchableOpacity>
  );
}

export default Button;
Now let’s create the test file Button.spec.js
...

describe('Rendering', () => {
  it('should match to snapshot', () => {
    const component = shallow(<Button label="Submit" />)
    expect(component).toMatchSnapshot()
  });

  it('Button renders correctly', () => {

    const tree = renderer.create(<Button />).toJSON();
    expect(tree).toMatchSnapshot();

  });
});
Jest will accept both extensions and will append file extension as necessary for auto generated files such as snapshots.
Since this is the first time we run this snapshot test, Jest will create a snapshot file for us inside the folder __snapshots__.
boom

Events Testing

We can either check that the state was updated by accessing it directly through the root instance’s instance.state or by checking the rendered text value.
// Events.js
import React from 'react';
import {
  View,
  Text,
  TouchableOpacity,
} from 'react-native';

class Events extends React.Component {

  state = {
    counter: 1,
  }

  _setCounter = () => {
    this.setState(prevState => (
      { counter: prevState.counter + 1 }
    ));
  }

  render() {
    const { counter } = this.state;
    return (
      <View>
        <TouchableOpacity onPress={this._setCounter}>
          <Text>{counter}</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

export default Events;
// Events.spec.js
...

it('updates counter when clicked', () => {

  const inst = renderer.create(<Events />);
  const button = inst.root.findByType(TouchableOpacity);
  const text = inst.root.findByType(Text);

  expect(inst.root.instance.state.counter).toBe(1);

  button.props.onPress();
  
  expect(text.props.children).toBe(2);
});
We render the <Events>, we can use findByType to find the TouchableOpacity and call its onPress function.

Events Hook Testing

How to testing component use Hooks ? For example:
// EventsHook.js
import React, { useState } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
} from 'react-native';

const EventsHook = () => {

  const [number, _setNumber] = useState(1);

  return (
    <View>
      <TouchableOpacity onPress={() => _setNumber(number + 1)}>
        <Text>{number}</Text>
      </TouchableOpacity>
    </View>
  );
}

export default EventsHook;
// EventsHook.spec.js
import React from 'react';
import {
  Text,
  TouchableOpacity,
} from 'react-native';
import renderer from 'react-test-renderer';
import { waitForElement } from 'react-native-testing-library';
import EventsHook from './EventsHook';

describe('EventsHook', () => {

  it('calls setCount with count + 1', async () => {

    let inst;
    inst = renderer.create(<EventsHook />)

    await waitForElement(() => {
      const button = inst.root.findByType(TouchableOpacity);
      const text = inst.root.findByType(Text);

      button.props.onPress();

      expect(text.props.children).toBe(2);

    })

  });

});
Result test final zap zap
final

sparkles Wrap up

These are few basic ways you can start unit testing your React Native codebase with Jest and Enzyme. They both have great documentation so there is no reason you should not unit test your codebase anymore.

Comments

Popular posts from this blog

Cách sử dụng Nmap để scan Port trên Kali Linux

Port là gì ? Có rất nhiều lớp trong mô hình mạng nói chung, lớp vận chuyển đóng vai trò cung cấp các thông tin liên lạc giữa các ứng dụng hệ thống với nhau, và lớp này thì được kết nối với Port (Cổng). Một số điều lưu ý mà bạn cần biết về port - Port là một số hiệu ID cho 1 ứng dụng nào đó. - Mỗi ứng dụng chỉ có thể chạy trên một hoặc nhiều port và mang tính độc quyền, không có ứng dụng khác được chạy chung. - Chính vì tính độc quyền nên các ứng dụng có thể chỉnh sửa để cho phép chạy với một port khác. - Port cũng có phân chia làm Internal và External . - Số hiệu từ 1->65535. Một số thuật ngữ mà bạn cần nắm rõ Port: Là một địa chỉ mạng thực hiện bên trong hệ điều hành giúp phân biệt các traffic khác nhau của từng ứng dụng riêng lẻ Internet Sockets: Là một tập tin xác định địa chỉ IP gắn kết với port, nhằm để xử lý dữ liệu như các giao thức. Binding: Là quá trình mà một ứng dụng hoặc dịch vụ (service) sử dụng Internet Sockets để xử lý nhập và xuất các dữ liệu ...

Sử dụng react-bootstrap-table trong ReactJS

Xin chào các bạn ! Hôm nay mình xin giới thiệu các bạn về một library của JS đó chính là react-bootstrap-table. Như cái tên đã nói lên tất cả công việc của nó là gì rồi nhỉ, haha. Về mặt cá nhân mình thì thấy nó khá hay trong việc xây dựng lên 1 hệ thống các table với tốc độ phải nói là "Quá nhanh quá nguy hiểm".  Không nói nhiều nữa bắt đầu thôi nào ! Các bạn có thể xem qua nó tại địa chỉ của nó tại github:  https://github.com/AllenFang/react-bootstrap-table Về cơ bản tài liệu của nó cũng rất dễ để sử dụng, các bạn chỉ cần làm theo hướng dẫn của nó là đã có thể tự mình tạo ra được rồi. :) Để sử dụng library này kết hợp với ReactJS   thì tất nhiên các bạn phải install React trước rồi, sau đó các bạn download library react-bootstrap-table về bằng cách download file zip hoặc sử dụng git như sau: $ git clone https://github.com/AllenFang/react-bootstrap-table.git $ cd react-bootstrap-table $ npm install Sau đó chỉ cần npm start là đã có thể xem được các ví dụ củ...

Google Hacking - Kiến thức cơ bản mà Pentester thường bỏ qua!

Mình để ý thấy có nhiều bạn đang bị lu mờ giữa 2 khái niệm Pentest và Hacking. Thực sự thì cũng sẽ khó có thể phân biệt rõ ràng nhưng "Hacking" là cụ từ bạn sẽ sử dụng khi bạn tìm thấy lổ hổng ( cả về lổ hổng logic và lổ hổng đến từ tech ) và khai thác được lổ hổng đó. Còn Pentest theo mình thì chỉ là kiểm tra đánh giá tính bảo mật của một hệ thống hoặc một server nào đó thôi. OK! Bắt đầu với chủ đề mà mình đề cập tới sau đây. Có nhiều bạn trẻ hỏi mình: "Làm sao để có thể nhanh chóng tìm được lỗi từ ứng dụng web?". Câu trả lời của mình là:"Hãy bắt đầu với Google Hacking." Google Hacking là một thuật ngữ mà gói gọn một loạt các kĩ thuật cho phép truy vấn trên công cụ tìm kiếm Google.com, đôi khi được dùng để xác định các lổ hổng trong các ứng dụng web cụ thể.(Cụ thể như thế nào thì mình sẽ cố gắng giải thích tiếp trong giới hạn kiến thức mà mình biết). Bên cạnh việc truy vấn từ google có thể tiết lộ các lỗ hổng trong các ứng dụng web, Go...