Many Domestic Internet companies have their own QA teams, and most front-end development does not need to write their own test code to test the operation of the program. But as the front to deep water area, each front team infrastructure development will create a number of common base class library, compared with the ever-changing business code, the base class library code relatively “stable”, and the base class library involves the stability of the top business application, write a good test for the code base is very necessary.

Front end test

Software testing is an investigation conducted to provide stakeholders with information about the quality of the Software Product or Service Under Test. — CEM KANER, “Exploratory Testing” (NOVEMBER 17, 2006)

“Software testing is an investigation conducted to provide stakeholders with information about the quality of the software product or service under test.”


From an engineering point of view, software testing is an automated tool that helps developers find errors in development as early as possible.

Specifically for front-end development, front-end testing is used to verify that what users see and use in the application works as expected.

Test categorization

Different types of tests cover different aspects of the project. Ideally, we should use different types of tests to cover different types of problems.

Unit testing

Responsibilities: Check and validate the smallest testable units in the software.

Unit testing refers to checking and verifying the smallest testable unit in software, that is, a test unit is often an atomic function. It focuses on the individual components of a program and ensures that they work as expected.

  • The value of unit testing: It forces developers to think about the possible results of a function. Usually, after testing, we find some missing edge scenarios, which can help us fix and complete the function in our code.
  • Characteristics of unit tests: small and simple

Here’s an example of unit testing a function:

// The function being tested
const sayHello = (name) = > {
  if(! name) {return "Hello human!";
  }

  return `Hello ${name}! `;
};
Copy the code
// Test the code
describe("sayHello function".() = > {
  it("should return the proper greeting when a user doesn't pass a name".() = > {
    expect(sayHello()).toEqual("Hello human!")
  })

  it("should return the proper greeting with the name passed".() = > {
    expect(sayHello("Evgeny")).toEqual("Hello Evgeny!")})})Copy the code

The Expect function receives the input we want to verify, and the toEqual function receives the result we expect.

expect(sayHello()).toEqual("Hello human!")
expect(sayHello("Evgeny")).toEqual("Hello Evgeny!")
Copy the code

Integration testing

Responsibility: Test integration and interaction between independent units

Unit tests check the behavior of the smallest testable units, but programs in the Real World rarely consist of units that run separately. That’s why we rely on integration testing, which is responsible for making sure the units work perfectly together.

Add sayHello() from the unit test to the React Component for the integration test:

export const Greeting = () = > {  
  const [showGreeting, setShowGreeting] = useState(false);  

 return (  
   <div>  
     <p data-testid="greeting">{showGreeting && sayHello()}</p>  
     <button data-testid="show-greeting-button" onClick={()= > setShowGreeting(true)}>Show Greeting</button>  
   </div>
 );  
};
Copy the code
// Test the code
describe('<Greeting />'.() = > {  
  it('shows correct greeting'.() = > {  
    const screen = render(<Greeting />);  
     const greeting = screen.getByTestId('greeting');  
     const button = screen.getByTestId('show-greeting-button');  

     expect(greeting.textContent).toBe(' ');  
     fireEvent.click(button);  
     expect(greeting.textContent).toBe('Hello human! ');  
 });  
});
Copy the code

The real integration test starts on line 7:

 expect(greeting.textContent).toBe(' ');  
 fireEvent.click(button);  
 expect(greeting.textContent).toBe('Hello human! ');  
Copy the code

The test program first checks to see if the P tag is empty. The button is then clicked by simulating the click event. Finally, check if the P tag contains “Hello human!”

This completes a simple integration test to verify that the sayHello function works as expected with the Greeting component.

End-to-end testing (E2E)

Responsibility: Tests that the entire flow of the application from start to finish is performing as expected.

End-to-end testing is only concerned with how users interact with the program, not the implementation and specific code behind the program. From a front-end perspective, end-to-end testing simulates the user’s real-world usage, telling the browser what to perform, click, and type, and we can create various interactions to test different features and processes that the end user will experience.

Here is an end-to-end test case using Cypress as the test tool library. Assuming the front-end’s local URL is http://localhost:3000, test it as a user:

describe('Greetings functionality'.() = > {  
  it('should navigate to greetings page and confirm it works'.() = > {
		// First cy.visit will visit the URL 
      
    cy.visit('<http://localhost:3000>')  
		// Get the nav button and trigger the click event to jump to the page where the Greeting component is located.
    cy.get('#greeting-nav-button').click()
		// Enter Evgeny in greeting-input
    cy.get('#greetings-input').type('Evgeny', { delay: 400 })  
    cy.get('#greetings-show-button').click()  
    cy.get('#greeting-text').should('include.text'.'Hello Evgeny! ')})})Copy the code

The end-to-end testing described above looks very similar to integration testing, but the main difference is that the end-to-end testing is run in a real browser.

Front-end Testing Pattern

Asserts that an Assertion

Assertions are used to verify that the variable under test contains the value we expect, for example:

// Chai expect (popular)
expect(foo).to.be.a('string')
expect(foo).to.equal('bar')

// Jasmine expect (popular)
expect(foo).toBeString()
expect(foo).toEqual('bar')

// Chai assert
assert.typeOf(foo, 'string')
assert.equal(foo, 'bar')
Copy the code

Reconnaissance soon

The Test Spy is a function that records input parameters, return values, this values, and exceptions thrown (if present) for all of its calls.

Spy is used in integration testing to ensure that the side effects of the process are as expected. For example, in the following example, record how many times a function like execute is called.

class Child {...execute(){... }... }class Father {
  constructor() {
    this.child = new Child()
  }
  ...
  execute(){...this.child.execute()
    ...
    this.child.execute() ... }... } it('should call child execute twice when father executes'.() = > {
  const father = new Father()
  
	// Create a Sinon Spy to Spy on object.method
  const childSpy = sinon.spy(father.child, 'execute')

  // call the method with the argument "3"
	// Call this method with input parameter 3
  father.execute()

	// Make sure child.execute is called twice
  assert(childSpy.calledTwice)
})
Copy the code

Stubing

A Stub can be understood as a Spy with pre-programmed behavior that replaces the selected methods of an existing module with user-supplied functions.

In the following example, the test code checks whether user.isValid() always returns true during the test:

// Sinon
sinon.stub(user, 'isValid').returns(true)

// Jasmine
spyOn(user, 'isValid').andReturns(true)
Copy the code

Mocks

Mocks are used to disguise certain modules or behaviors to test different parts of the process.

For example, the Sinon library can mock a server to ensure offline, fast, and expected responses when testing a process:

it('returns an object containing all users'.done= > {
  
  // Mock a Server instead of a local network call
  const server = sinon.createFakeServer()
  server.respondWith('GET'.'/users'[200,
    { 'Content-Type': 'application/json' },
    '[{ "id": 1, "name": "Gwen" }, { "id": 2, "name": "John" }]'
  ])

  // Make a request to the network you mocked earlier
  Users.all()
    .done(collection= > {
      const expectedCollection = [
        { id: 1.name: 'Gwen' },
        { id: 2.name: 'John' }
      ]
      expect(collection.toJSON()).to.eql(expectedCollection)
      done()
    })
  
	// Respond to the request
  server.respond()
  
  // Remove fake Server
  server.restore()
})
Copy the code

Snapshot Testing

A typical Snapshot Testing Case can render a UI component to take a Snapshot and then compare it to another Snapshot file. The following example shows a snapshot test on a Link component:

it('renders correctly'.() = > {
  
	// Create a Link component instance
  const linkInstance = (
    <Link page="<http://www.facebook.com>">Facebook</Link>
  )
  
	// Create a snapshot of the component data
  const tree = renderer.create(linkInstance).toJSON()
  
	// Compare with the previous snapshot
  expect(tree).toMatchSnapshot()
})
Copy the code

In addition to comparing snapshots of UI component data, Snap Testing can also compare other data types, such as the Redux Store or the internal structure of different units in an application.

Browser Controllers are Browsers Controllers

Developers often use browser controllers to simulate user behavior, such as clicking, dragging, typing, and navigation.

Common browser controllers include Puppeteer and Cypress.

Test launchers

The test launcher is used to execute your test code in different environments (Browser/Node.js). Typically, it launches your test file based on the configuration provided (such as what browser to run, what Babel Plugin to use, and so on), and provides Assertion, Mock, and so on.

Popular test initiators in the industry are:

Karma, Jasmine, Jest, TestCafe, Cypress…

Testing Utilities

Test Launchers provide capabilities like Test Runner, Assertion, and others, regardless of the specific UI rendering environment. Specific to the rendering environment provided by different UI class libraries, we also need the response test assistant tools, such as Enzyme and React Testing Library, which provide the rendering environment of the React component, DOM operation and query ability, etc.

Testing Structure

Test structure refers to how your test code is organized. The organization of test code usually follows the BDD(behavior-drive developement) structure, for example:

describe('calculator'.function() {
  // describes a module with nested "describe" functions
  describe('add'.function() {
		// Declare specific expected behavior
    it('should add 2 numbers'.function() {
       //Use assertion functions to test the expected behavior. })})})Copy the code

Take a look at the React code test

There are a number of ways to test React code, which generally fall into two categories:

  • Test a component tree: Render the component tree in a simplified test environment and do assertion checks on their output (unit tests + integration tests)
  • Run the full application: Run the entire application in a real browser environment (i.e., end-to-end testing)

React recommends using the Jest + React Testing Library.

Create React App Jest + React Testing Library Jest + React Testing Library

As you can see, the scaffolding automatically generates a repository that inherits the Jest + React Testing Library and contains app.js and its test code app.test.js.

// App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="<https://reactjs.org>"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
Copy the code
// App.test.js
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link'.() = > {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
Copy the code

The Render function in the React Testing Library takes any JSX and renders them. After rendering is complete, we can access the components under test.

Let’s run the test above to see if the Learn React link is included in the App component.

The test environment

The above test was not run in a real browser, but in a jsDOM simulated virtual browser, a lightweight browser implementation that runs inside Node.js.

End to end testing

End-to-end testing wants to test how real browsers render the entire application, get data from real apis, use sessions and cookies, and navigate between different links. Jest and React Testing libraries are not enough, we also need libraries that can provide real-world browser environments, such as Cypress and Puppeteer.


(More details about the React test process are not covered here. This module provides a brief introduction to the test environment of the React application. You can visit the test library website for more detailed test tutorials.)


References:

  1. An Overview of JavaScript Testing in 2020

  2. Front-End Testing is For Everyone

  3. Spies – Sinon.JS