CS312 - Assignment seven [Extra Credit]

Topics

Testing - Basic concepts

For most testing scenarios, you need two things. You need a test runner, which is responsible for running your tests and reporting on the results. You also need an assertion library, which provides ways to write tests. For our purposes, we will take a look at Jest, which is also developed by Facebook, so it has close ties to React (though neither requires the other). Jest provides both the test runner and the assertion library, so we have most of the pieces we need. It is pretty simple to use, but it is not necessarily the best (a matter of opinion at best) or most frequently used. We are going to use it because it is already bundled in when we use the Create React App tool.

The basic idea is best illustrated with an example from the Jest documentation. Suppose you have written this JavaScript module, sum.js (note that this is using Node.js style module syntax).


function sum(a, b) {
  return a + b;
}
module.exports = sum;

To test it, you might create a test file called sum.test.js (the name is unimportant, but it can be useful to have the filename be something easy to recognize).


const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

There are two things we are getting from Jest here. The first of these is the test(string, fn) function. This is a basic test. The test takes in a string description that will be printed when the test is run, as well as a function that will provide the body of the test.

The function should contain one or more assertions – tests of state or values in your code. The expect(value) function takes in a value generated by your code in some way and returns an "expectation object". To turn this into a test, we apply a matcher to test the value. There are a number of different matchers, and the one above should be fairly obvious. Jest will run all of your tests for you and keep track of how many tests pass and how many fail.

As mentioned above, you can have multiple assertions within a single test. All of the assertions should contribute in some way to the test.

Jest provides another function called describe, which allows us to wrap multiple tests together into a "suite". These tests can be loosely coupled. Perhaps they all test the same component or approach testing a function from different directions. For example, with our initial example, you could have one test for positive sums, one test for negative sums, one for mixed sums, and one for edge cases. Write a test suite would look like this:


describe('Sum tests', () =>{

  test('positive sums', () => {
    expect(sum(1, 2)).toBe(3);
    expect(sum(10, 2)).toBe(12);
  });

  test('negative sums', () => {
    expect(sum(-1, -2)).toBe(-3);
    expect(sum(-10, 2)).toBe(-8);
  });

})

Testing - setup

We are going to write some tests for the simplepedia client that I gave you earlier. It is already configured to use Jest, so there is very little setup required.

At the top-level of the project, create a folder called __tests__. Jests will automatically look inside this directory for tests. Any .js file that you place inside this directory will be considered to be a test. You can start testing by typing npm test. This will start up Jest and run your tests. Just like with our development server, Jest will monitor all of the directories in the project and rerun our tests if the any of the tests or project files change.

At this point, we can use Jest to perform basic testing. This works best for pure functions, in which there is no state or side effects, and the output is purely a function of the inputs. Unfortunately, what we want to test is React components. So, we need some more libraries. Install the following:


npm install react-addons-test-utils --save-dev
npm install enzyme --save-dev

Note that we are saving these to our development dependancies. We don't need these for deployment.

This installs Enzyme, which is a library that will help us to "render" our components so we can test them.

Create the test file

Okay we are just about ready now. Create a file called tests.js in your __tests__ directory (normally we would split this up more, but we are not going to write that many tests).

In your file, add these lines:


import React from 'react';

import {shallow, mount, render} from 'enzyme';

import IndexBar from '../src/components/IndexBar';
import Article from '../src/components/Article';


const articles = [
  {_id:1, title:'Alpha Centauri', extract:'An alien diplomat with an enormous egg shaped head', edited:'2017-05-08'},
  {_id:2, title:'Dominators', extract:'Galactic bullies with funny robot pals.', edited:'2017-05-08'},
  {_id:3, title:'Cybermen', extract:'Once like us, they have now replaced all of their body parts with cybernetics', edited:'2017-05-08'},
  {_id:4, title:'Auton', extract:'Plastic baddies driven by the Nestene consciousness', edited:'2017-05-08'},
  {_id:5, title:'Dalek', extract:'Evil little pepper-pots of death', edited:'2017-05-08'}
];

We are going to be testing the Article and IndexBar components, so we are important them along with React. We are also importing three functions from Enzyme, which we will get back to in a moment.

The second part of this is mock article list. We will frequently mock up inputs to be more controlled so we can better reason about them.

Snapshots – testing the Article component

Jest provides one approach to testing our components called snapshots (docs). These are used during development to make sure that aspects of our interface don't change unexpectedly as we are working on other parts of the project. We take a snapshot of the component at a time when we like the way it looks. Jest saves this, and the every time we run our tests, it regenerates the component and compares it to the snapshot. If the component changes, we will get notified, which is our cue to either fix the offending code, or to take a new snapshot because the change was intentional.

Note that the snapshot is not a literal picture, it is a JSON description of the component that can be quickly compared.

Let's start by creating a snapshot of the Article component. Create a new test suite called "Article tests". Create one tests inside and give it the description "Article snapshot test".

To create the snapshot, we need to render the component. This is where Enzyme comes in. We will use the shallow renderer. This doesn't dive into child components – it just gives us the top level components. In theory, this makes it faster.


const article = shallow(<Article article={articles[0]}></Article>);

Note that we can still write JSX here. You should also note that we have just grabbed a random article from our mock article list.

Now, we need to tell Jest to compare this to our snapshot.


expect(article).toMatchSnapshot();

This is all we need. When you put it together, it should look like this:


describe('Article tests', ()=>{
  test('Article snapshot test', () =>{
    const article = shallow(<Article article={articles[0]}></Article>);
    expect(article).toMatchSnapshot();
  });

});

Note that we didn't write anything to generate the snapshot. Jest will do that automatically the first time we run the test.

On the command line, run npm test if it isn't already running and you should see your test get run.

You can also take a look at the snapshot in the __snapshots__ directory Jest makes in your __tests__ directory after the first run. Try making a change to the Article component and rerunning the test to see that the test will now fail.

Testing IndexBar

We are going to test some aspects of IndexBar to illustrate some of the other things that we can do.

Start by making a new test suite called "IndexBar tests".

For your first test, create a snapshot for the IndexBar.

Testing props

A primary task of the IndexBar component is to parse the articles and determine the section list. This list of sections is passed to the Sections component as props. So, for this test, we are going to dive into the IndexBar component, find the Sections and look at its props.

Start by doing a shallow render of the IndexBar.

To find the Sections component, call the find() (docs) function on the wrapped component returned by shallow(). Pass in 'Sections', and find() should return the Sections component.

Given a wrapped component, we can access the props with the 'props()' function. This will return the props as an object.

The list of sections is passed to Sections in the prop sections.

So, given the Sections component n a variable called sectionsComponent (attach no importance to this name), you can access the sections list with sectionsComponent.props().sections.

Write a test using the toEqual matcher to make sure the that the right array is being passed along. (what should it be given our list of articles?)

Testing state

Another thing we would want to do is test the state of a property. We have a function called state(key) on our Enzyme wrapped components that will return the state associated with the passed in key. If you look at the IndexBar component, you will see one piece of state is the section. By default, when there isn't a current article, this is set to the first section in the list (so it should be 'A' for our articles).

Write a new test that checks to make sure that the state of section in our IndexBar is, in fact, 'A'.

Of course, when we do have a currentArticle, then that should be used to determine the current section. Write another test that passes in the second article, and test to make sure that the section is now set to "D".

Uh oh. It isn't, is it? Fix IndexBar so that this test passes.

User interaction

Another key thing to test is how our interface changes when users start poking at it. Enzyme allows us to simulate some of the basic actions supported by HTML.

We are going to test that clicking on a section heading actually changes the section. We need to start by finding the component we are going to click on.

To simulate actions, we need a richer version of our component than is provided by the shallow function. Instead, we will use mount(), which will virtually render your component and mount it (calling it a second time will fire off componentDidMount).

We will also use a different function for finding the right component: findWhere() (docs). The findWhere() function takes in a function that returns a Boolean value. The function takes a single argument (the "predicate"), which is the current node being examined by the findWhere function (think of it like the filter function on arrays).

In order to find the right element, we need to know how to test elements for various properties. The two we are interested in right now are the type (accessed with node.type()), and the text of the node (accessed with node.text()). Implicit in these examples is that node is what you called the argument of your findWhere predicate. If you used a different name, replace it as needed. For our purposes, we would like the type to be "li" and the text to be one of the other sections other than "A".

The findWhere function will return the element we want provided it finds one match. We can then call simulate('click') on this element to simulate a user clicking the section heading.

Once you have simulated the click, check the section state of the IndexBar component to make sure it has changed.

This only barely scratches the surface on the topic of testing, but hopefully getting your feet wet here will serve you well later down to road.

Grading

Points Criteria
1 Project submission/setup
1 Snapshot test
1 Prop test
2 State test
1 IndexBar repair
1 Click test

To hand in your work, please make a copy of your working directory, rename the copy username_hw07, remove node_modules from it, and then zip it up. I would rather regenerate node_modules when I look at your work rather than having you submit the huge directory, so really, please don't include it.

You can then submit the zipped folder on Canvas.