DEV Community

loading...

The What, Why and How of React (Testing)

mangel0111 profile image miguel-penaloza Updated on ・25 min read

Hello, today we're going to talk about one of the most important parts(or should be one) in a development process, no matter the framework, the language or if you're a frontend or backend, the tests are vital to verify that your codes really do what was coded to do. In this post, We're going to check the ways to test a react application, understand what means real coverage, the principal's and popular libraries used and try to understand when is the best moment to test your code, so let's go.

What means Testing a React application

In most of the interviews that I been made, to work as a developer, they always ask about TDD(Test Driven Development), BDD(Business Driven Development), unit testing, automation testing and so, but at the same time in most companies for which I have worked they don't really make tests, or rather, they write some test as an obligation that doesn't give any value to the code, this's often caused because they don't have a culture of testing.

So, you have developers who maybe understand how to test, they maybe know why to test, but the test is always like this painful task that you have to do, without realizing that a good test is a bug-less(no bugless, that's a myth) in production, and this goes for any framework or library, no just React, so lets see the principal concepts about testing and try to understand what really means and verify if this can help you in your job.

Unit testing
This's a very simple but powerful concept, you need to create a test who check one unique part of your code, the goal if have a unit test who verifies a premise, that a function called will answer the same that you're expecting to receive.

Understanding that, you know that you need to have so many unit tests as you can, to verify that your entire code doesn't fail and be completely sure that any new code who change the current behavior or broke something not related with that specific development task will catch it for your battery of tests, let's see an example where these are my premises.

  • My calculator receive two numbers and returns the sum of both.
  • If I pass a no number as a parameter, that parameter is taken as a zero.

These 2 premises are our base, is what we need, we can say that's the acceptance criteria for the code that we need to write. The idea here is to create unit tests for each acceptance criteria, to verify that our function complies with both acceptance criteria always, no matter if your codes changes in the future, the 2 premises should be respected.

TDD (Test Driven Development)
This's a term that always appears in the interviews, but what's TDD? is a programming practice where you write the test before code, that means that you need to understand what you have to do before starting to code, means that you write your test to expect to receive a correct answer (Test Before Code), create an empty function that will fail, and then fix your code to return the expected answer, and then continue the process with the next task. let's go and try to implement our calculator with TDD:

  1. We need to create a function to add two numbers, so let's write the test before the code, the test should expect the correct answer and create an empty function that will fail.
// Sum function 
var sum = (a, b) => return 0; // This function always return zero

// Your test 
var shouldAddCorrectly = () => {
    return sum(2,2) === 4; // False
};

Enter fullscreen mode Exit fullscreen mode

In the code above the function, shouldAddCorrectly is our test and is expecting to receive 4, we are trying to add 2 and 2, but the add function is failing and returning 0, we have a correct unit test for a wrong code, what we need is fix the sum function.

// Sum function 
var sum = (a, b) => return a + b; // This function now is working well

// Your test 
var shouldAddCorrectly = () => {
    return sum(2,2) === 4; // true
};

Enter fullscreen mode Exit fullscreen mode

As you can see the test and the code now works, right now we're not using any library or framework to test, is pure javascript. The second premise is indicating us that we need to check when one of the parameters is not a number if it is a number use it, otherwise, this params will be a zero, so we create the test to validate that.

// Sum function 
var sum = (a, b) => return a + b; // This function now adding but not filling all the requirements.

// Your tests
var shouldAddCorrectly = () => {
    return sum(2,2) === 4; //true
};

var shouldAddCorrectlyWhenFirstParamIsNotANumber = () => {
    return sum('Something',2) === 2; // false, because is returning "something2"
};
Enter fullscreen mode Exit fullscreen mode

Here we have that our new test is failing, but also our test is correct, the answer should be 2, not 'something2', now we fix the code, and both tests are passed.

// Sum function 
var checkNumber = (number) => isNaN(number) ? 0 : number;

var sum = (a, b) => {
    var firstParam = checkNumber(a); 
    var secondParam = checkNumber(b); 
    return firstParam  + secondParam;
}

// Your tests
var shouldAddCorrectly = () => {
    return sum(2,2) === 4; // true;
};

var shouldAddCorrectlyWhenFirstParamIsNotANumber = () => {
    return sum('Something',2) === 2; // true
};
Enter fullscreen mode Exit fullscreen mode

Benefits of TDD

  • We can use this to avoid the bad practice of try to testing everything at the end of the development, if you implement TDD, you will have all your test done before your code.
  • You're going to understand better your code before the start.
  • This will force you to reduce your functions to small parts of logic, and this's always good. Avoid overcomplex code should be our golden goal.
  • You can trust in your code, and make sure that you can detect errors in the dev process before the integration.

But, if TDD is so good why is so hard to implement in your process? well, the big problem with TDD is that write a test include an amount of time and effort that some projects don't have and most of the teams use this 2 classic excuses to don't even try TDD.

  • We don't have time.
  • We're very sure that our code works.

To really implement TDD or any other methodology, what we need to have, is just have a simple thing called culture of testing, and we're going to talk about that later.

BDD (Business Driven Development)

BDD is an evolution or the testing process, TDD verifies small part with unit tests, BDD writes a test that's not necessary a unit test, to verify that the business cases are taking in account in the development and not just the logic.

Because you can have a very good code who works perfectly, the test that verifies that the code works on multiple scenarios, but at the end, the code fails because doesn't fit the business requirements, so basically BDD is verify behavior instead of implementation, let's see an example.

We have the code writen before, but now my business requires that instead of taking the no numbers parameters as zero, now we need that the calculator answer "There's an error in your parameters, please verify, and thanks for use this calculator!" when you provide a no number parameter, this change is a business requirement and we need to validate that works.

// Sum function 

var sum = (a, b) => {
    if(isNaN(a) || isNaN(b)) {
        return "There's an error in your parameters, please verify, and thanks for use this calculator!";
    }
    return a + b;
}

// Your tests
var shouldAddCorrectly = () => {
    var answer = 4;
    return sum(2,2) === 4; // true
};

var shouldAddCorrectlyWhenFirstParamIsNotANumber = () => {
    var answer = 2;
    return sum('Something',2) === "There's an error in your parameters, please verify, and thanks for use this calculator!"; // true
};
Enter fullscreen mode Exit fullscreen mode

We now have a test who verifies business instead of just implementation, we can have a more complex test using BDD, for example, in the How section we will see how to make that in React

Function Test, Automation Test, and Integration Test
The functional test is the test made by a QA, for a human (if we can call QA's humans), the QA now is the one who validates an entire application where they need to verify all the requirements and scenarios, the automation tests are the same functional test but this time executes by a tool, selenium is one of the most popular frameworks to run automated test now days.

The Integration tests are made to validate functionalities that are operatives and deployed, we need to understand that the unit tests are made to focus on the details of individual implementations and these others tests are to validate flows.

Testing Culture
So, we defined some of the most important concepts of testing, now we need to talk about the testing culture, as we say before the problem with the test is that the most of the developers don't feel write test as part of the development, instead is an extra task, is a boring assignation that block you to continue development cools stuff.

What we need is a Culture of Testing and this only can achievement when the developer feels that test are giving value instead of more job, what we need to do is follow this rules and very soon we're going to see the real value of the test.

  • Thinks before code, Testing is the best way to understand what're you going to code, try to identify the problem before the start, instead of think how implement something, try to understand what factors can make your code fails, is a changeset mind that will give you the power to understand what the heck are you doing and how do it better.
  • When my code Compile and Run I can finish a task, but without test we do not finish the story You can finish all your tasks, write all your code, but the job is not ready until everything is done, that means that everything should work ok, without test you don't know that, so you can't know if your code works ok.
  • A failing test is not a bad thing, we the humans have this psychological need to see everything ok, something failing means that we're wrong, but sometimes a failed test can be a good thing too, because this blocks you to merge something wrong, you should give thanks to the tests to avoid the shame of being that commit who block everything to everyone.
  • Make it simple, There's a lot of methodologies and patterns like YAGNI (You're not going to need it) or KISS (keep it simple stupid) that helps you to write better code, but using TDD is the best way to really get that.
  • The real goal is not the coverage, we often see teams where the rule is to have more than 80% of coverage or any other number where they feel comfortable, but what we need to understand is that coverage means nothing is we have bad tests, so instead of try to fill a number of coverage, try to write real test who validate business and logic where matter.
  • Our tests need to be bulletproof, If we can remove a line in our code, and the test pass it anyway, our tests are not ok.

These are simple rules that will help you to create a culture of testing on all the teams.

Why Test in React.

You need to test because you will have:

  • Quality Code: You are sure that the code does what we expect.
  • Design focus on the needs: You understand the requirements, you design based on that, and you build thinking on that.
  • Less debugging more coding: With more test, fewer errors you will have and you can focus on the more complicated and funny tasks.

How Test in React

Now we get to React, how testing our application? and not just testing to get a coverage, instead, we're going to see how to make real tests and be totally sure that our coverage means something. What we're going to use to test is the most popular framework to do that on React Jest, this library can be used not only for React, but works very good, also, we're going to use Enzyme that's a util to test React applications who allows creating mocks and shadows of our components, and (Istambul)[https://istanbul.js.org/] that helps us to collect the coverage.

First, we need to understand what part of our React environment can be tested, for that we can split our tests by scope or type of element.

How to test Components

React is a library who help us to create encapsulated Views, where we can handle his states and add so many logic as the component requires, so let's start by the beginning, and let's see the base concepts of a React component

Understanding the Lyfecycles
All the components start with a mounting process if the component is updated have an updating process, and an 'unmounting' process when the component is removed, understand this's is important because a good test should verify the behavior of your component in all his states or lifecycles. Each process will call different methods that in some moment we're going to need to mock or dispatch.

These methods are called when a component is created (Mounting)

  • contructor Will receive some props and should start the states
  • getDerivedStateFromProps almost never used is static, doesn't have access to the props or state of the component
  • render where the magic happens
  • componentDidMount This's the most common method used to make a request for data

These methods are called when a component detects a change in his props or state (Updating)

  • getDerivedStateFromProps Also static.
  • shouldComponentUpdate this function is used to avoid re-render after an update if you include that kind of logic in your component you should test it.
  • render the magic again.
  • componentDidUpdate Is the best place to make a request for any kind of data.

And by the end, when the component is removed, this function is called:

  • componentWillUnmount This's used to clean the DOM and cancel all the possible request of subscriptions made by the component.

NOTE: These're the methods currently used in September 2018 to React 16, this flow can change and some methods could be deprecated in the future or non-accessible in the previous versions of React.
NOTE 2: Is very important to understand the reason behind each method to use them correctly, understanding the reasons you can understand what test

Understading States and Props
The components also have states and props, the props are information provided by the parent component, the one who calls him, and the state is declared in the construction, and have the information of the component, is the component the only one who should manipulate his state, and the props are sacred, never should changed.

Shadow and Mounting

Manipulate changes on the state, if one of the ways to test a react component, the react components have functions bound to his elements like an 'onChange' on the inputs or 'onClick' on the buttons, so you can create a shadow or a mount of your component, then you should be able to click and change inputs or any other event imitating a real environment.

A Shadow is an isolation of your component, you will only render your component without his children, and a Mount will reproduce all the render flow, to use mount you will need to have DOM declared for the test, you can use JSDOM.

What we need to do is create a Shadow or a Mount of your component with Enzyme, that will allow you to have a component having his mounting and updating process, there you can change inputs and click buttons, and basically make all the possible interactions with your component, verify your state and call any of your methods, with that you can prove your uses cases with your tests.

Mock
With Jest you can mock some components to avoid complicating your test resolving external dependencies, to mock your component just write this after the import declarations:

jest.mock('the relative or absolute path of the js file that you want mock', () => `Mocked JS or whatever`);

Enter fullscreen mode Exit fullscreen mode

As you can see, you can mock anything and return whatever you need it, you can also use the default mock of Jest only passing the path of the component to mock if you wanna see more about this functionality read this

Now you know some basics of React, Jest, and Enzyme, let's see how to write some tests.

The first thing is to install all your dependencies:

npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json
Enter fullscreen mode Exit fullscreen mode

NOTE: If you use Babel, async to get or typescript you will need to include the jest plugin for your compiler, like babel-jest, async to get or ts-jest.

Before starts, you need to create 2 things, a configuration file and a setup file on your project, let's start with the setup file, it will be called jestSetup.json our src folder, in this file, we're going to initialize the Enzyme Adapter. That will help us to use Enzyme with React 16, for older versions you need to check what Adapter use, this's the mine:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });
Enter fullscreen mode Exit fullscreen mode

Now we need to define where to put our test, you can have a test folder where we're going to create all the tests for your code or you can put your test in the same location where you have your file to test, Jest will run as a test file everything who finish in .test.js or .spec.js.

Note: You can change this on the textRegex that I will show you later.

So you can have so many tests as you want and order them as you wish, we're going to make it in the same folder but this's up to you.

Now is the turn to the configuration file, you can have had an external file and include the flag --config=jestconfig.json on your test command, or you can just include in your package.json in a jest key. Anyway, the configuration should look like this:

{
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "resolver": "jest-pnp-resolver",
    "setupFiles": [
      "react-app-polyfill/jsdom"
    ],
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
      "<rootDir>/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}"
    ],
    "testEnvironment": "jsdom",
    "testURL": "http://localhost",
    "transform": {
      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "moduleNameMapper": {
      "^react-native$": "react-native-web",
      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
    },
    "moduleFileExtensions": [
      "web.js",
      "js",
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ],
    "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.js",
    "snapshotSerializers": [
      "enzyme-to-json/serializer"
    ]
  } "roots": ["test", "src"]
  }

Enter fullscreen mode Exit fullscreen mode

As you can see I use the default configuration provided with react-scripts 2, the last 2 lines, setupTestFrameworkScriptFile will indicate what's our setup File, and the snapshotSerializers will help us to avoid problems with Leak of Memory on javascript.

You can check the Jest Config documentation to understand better what include on your setup file.

To include Coverage we need to add npm run test -- --coverage on our command line to allow jest and Istambul generate a coverage report.

Finally write a test

If you get to this part, you already have all your configuration done, and you can start writing your test.

Matching Snapshots

The base example of Jest with React is shallow a component, manipulate his events and match snapshots, this test is ok, you will write your component to change his attributes, like the class name, or some data-attribute with each event.

In the example of Jest, they create a Link component, his class name is bind to the state with this: className={this.state.class}. Then they mock a user entering over the component (Hover) and leaving (Blur), and for each event, they create a snapshot.

The first time when you run the test, jest will create the base snapshot, that will look like this one:

// __tests__/__snapshots__/Link.react.test.js.snap
exports[`Link changes the class when hovered 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}>
  Facebook
</a>
`;

exports[`Link changes the class when hovered 2`] = `
<a
  className="hovered"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}>
  Facebook
</a>
`;

Enter fullscreen mode Exit fullscreen mode

The next time when you run your test, jest will verify that the component in each test is creating the same snapshot if for some reason the component is creating a different one will mark as a failed test. You can just override the previous snapshot, but you need to verify why is failing before update.

Pros

  • You can verify that your component keeps with the same behavior, and is returning the same HTML every time is rendered.
  • This will verify that the execution is done without problems, no exceptions are thrown
  • You should be able to create snapshots passing multiple props and check what's rendered.

Cons

  • Is not a common scenario or practice change an attribute of a component to reflect a state, so more than one snapshot by tests is not common.
  • Render a component and create a snapshot will pass the test over a lot of lines, that will increase your coverage, but this doesn't mean that you are testing your component, in this scenario you just render a component no creating a testing that validates a business or functional case.

Testing a real case.

In the most of the cases, what you need to do to trust on your code is create a test that validates that works as you expected, but what we need to write a test validating a real case? we need a DOM or at least a mocked DOM, where I can manipulate my component and basically emulate real interactions, for example.

  • If I create a Form, I should test adding values to the inputs, submit or cancel the form, and verify the values provided to the inputs.
  • If I create a dashboard where I can click over an icon and be redirected to somewhere, I should test clicking the icon.

Sound silly and very simple, but write test is just that.

Let's go with a Test!

I will use a project as an example that I write some while ago, where a dashboard of multiple profiles of gnomes are displayed on the screen, and you should be able to filter by name.
You can get the source code here.

Write a Test for the Dashboard and the search input.

So, what we need? what's expected on this? let's start with our tests using BDD, and the first thing need it defines what we expect to happen in multiple scenarios

  • Without any text on the search all the gnomes in the dashboard should be visible as an Icon.
  • If I write something and match with some profiles, only the gnomes profiles that match with that name should be displayed.
  • If I write something that doesn't match with any profile, none profile should be displayed.

So, for this component, we have 3 functional and business cases to test. What we have here in this project 2 kind of files that will be tested.

  • components and containers All my react views, I will test only the Dashboard (that includes the list of gnomes), the Gnome Box, that have the white box where I can see the details of the gnome displayed and a Gnome Details.
  • saga Is where a made all the transactions of the application, I will show you how to test this too.

This will represent the most important part of our Application and are the one who should test it, to be sure that our code works as we expected.

Testing the Dashboard

I create a simple Component, who receives a list of gnomes and display each one in a GnomeDetails, have a filter that modifies the current list ad this it is. A very common component used in a lot of places.

export class DashboardPanel extends Component {
    constructor(props){
        super(props);
        this.state = {
            filterText: ''
        };
    }

    filter(){
        const { gnomes }= this.props;
        const { filterText } = this.state;
        const gnomesFiltered = gnomes.filter(gnome => {
            if(filterText){
                return gnome.name.toLowerCase().includes(filterText.toLowerCase());
            }
            return true;
        });
        return gnomesFiltered;
    }

    render(){
        const { filterText } = this.state;
        const gnomesFiltered = this.filter();
        return (
            <Dashboard>
                <Options>
                    <Title>Gnomes</Title>
                    <Filter>
                        <Input
                            type="text" 
                            width="150px"
                            isFilter
                            title="Filter"
                            value={filterText}
                            onChange={({target})=> this.setState({ filterText: target.value })}
                        />
                    </Filter>
                </Options>
                <GnomesList>
                    {gnomesFiltered.length !== 0 ? gnomesFiltered.map(gnome => 
                        <GnomeBox 
                            key={gnome.id} 
                            gnome={gnome}
                        />): 
                        <p>No gnomes to display</p>
                    }
                </GnomesList>
            </Dashboard>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The first recommended test to do on a component is a snapshot, we can use the jest toMatchSnapshot and generate one that will have a backup of what was rendered on the first test, if anything changes this snapshot will fail, this's a normal test, I use the beforeEach to load the props for each test, and a simple test to create and verify the snapshot, like this:

import React from 'react';
import { mount } from 'enzyme';
import DashboardPanel from 'components/DashboardPanel';
import GnomeBox from 'components/GnomeBox';
import Input from 'components/Input';

let props = {
};

describe('Dashboard Panel', ()=> {
    beforeEach(()=> {
        props = {
            gnomes: [
                {'id':0,'name':'Tobus Quickwhistle','thumbnail':'http://www.publicdomainpictures.net/pictures/10000/nahled/thinking-monkey-11282237747K8xB.jpg','age':306,'weight':39.065952,'height':107.75835,'hair_color':'Pink','professions':['Metalworker','Woodcarver','Stonecarver',' Tinker','Tailor','Potter'],'friends':['Cogwitz Chillwidget','Tinadette Chillbuster']},
                {'id':1,'name':'Fizkin Voidbuster','thumbnail':'http://www.publicdomainpictures.net/pictures/120000/nahled/white-hen.jpg','age':288,'weight':35.279167,'height':110.43628,'hair_color':'Green','professions':['Brewer','Medic','Prospector','Gemcutter','Mason','Tailor'],'friends':[]},
                {'id':2,'name':'Malbin Chromerocket','thumbnail':'http://www.publicdomainpictures.net/pictures/30000/nahled/maple-leaves-background.jpg','age':166,'weight':35.88665,'height':106.14395,'hair_color':'Red','professions':['Cook','Baker','Miner'],'friends':['Fizwood Voidtossle']},
                {'id':3,'name':'Midwig Gyroslicer','thumbnail':'http://www.publicdomainpictures.net/pictures/10000/nahled/1-1275919724d1Oh.jpg','age':240,'weight':40.97596,'height':127.88554,'hair_color':'Red','professions':['Carpenter','Farmer','Stonecarver','Brewer','Tax inspector','Prospector'],'friends':['Sarabink Tinkbuster','Tinadette Wrongslicer']},
                {'id':4,'name':'Malbin Magnaweaver','thumbnail':'http://www.publicdomainpictures.net/pictures/10000/nahled/zebra-head-11281366876AZ3M.jpg','age':89,'weight':43.506973,'height':101.6974,'hair_color':'Black','professions':['Smelter',' Tinker'],'friends':['Fizkin Fussslicer','Cogwitz Chillwidget']},
                {'id':5,'name':'Zedkin Quickbuster','thumbnail':'http://www.publicdomainpictures.net/pictures/10000/nahled/1-1193219094.jpg','age':273,'weight':38.742382,'height':91.54829,'hair_color':'Red','professions':['Cook'],'friends':['Libalia Quickbooster','Whitwright Mystwhistle']},{'id':6,'name':'Emmadette Gimbalpower','thumbnail':'http://www.publicdomainpictures.net/pictures/20000/nahled/stingray.jpg','age':212,'weight':40.681095,'height':98.701645,'hair_color':'Green','professions':['Mason'],'friends':['Ecki Gyrobuster','Zedkin Nozzlespackle','Milli Clankswhistle','Libalia Magnatink']},
                {'id':7,'name':'Twizzle Chrometossle','thumbnail':'http://www.publicdomainpictures.net/pictures/10000/nahled/1-1275919724d1Oh.jpg','age':85,'weight':38.953087,'height':96.0678,'hair_color':'Red','professions':['Baker','Brewer','Tax inspector'],'friends':['Libalia Mystbooster','Zedkin Gyrotorque']},
                {'id':8,'name':'Malbert Tinkbuster','thumbnail':'http://www.publicdomainpictures.net/pictures/10000/velka/1-1248161543llOC.jpg','age':186,'weight':41.159805,'height':118.27941,'hair_color':'Gray','professions':['Baker','Mason'],'friends':[]},
                {'id':9,'name':'Kinthony Nozzlebooster','thumbnail':'http://www.publicdomainpictures.net/pictures/20000/nahled/baby-lamb.jpg','age':233,'weight':41.024612,'height':113.56545,'hair_color':'Red','professions':['Smelter','Miner','Tax inspector','Carpenter'],'friends':['Zedkin Clankstorque','Midwig Magnarivet']}]
        };
    });
    it('should match snaptshot dashboard', () => {
        const dashboardPanel = mount(<DashboardPanel {...props}/>);
        expect(dashboardPanel).toMatchSnapshot();
    });
});
Enter fullscreen mode Exit fullscreen mode

As you can see, here I have a test that sends props to a component, render it and everything looks ok, but can we say that we really test our component? We need to check our coverage report to really understand what's missing, if you run your test including npm test -- --coverage you will have a new folder coverage created and your root project, and you will find this file: /coverage/lcov-report/index.html, please open it on your favorite browser and you will see the coverage status of your project.

Let's search our DashboardPanel component and try to understand what's being reported.

Status Covarage

Status with only snapshot test

Wow! I have 90% of coverage on Lines and Statements, in functions we are over the 80%, they're big numbers, the branch is a little low, but in average we're ok right?, if we as a team decide to have a coverage of 80% I totally get it with this test, but my component is really tested? Let's see my code status:

Code status

As you can see, the code says something different than my previous numbers, I have some part of the code on red, this means that my test never gets there, and also some on yellow, this means that I have a conditional if that never was tested. so, we can see that my test is not really tested, I have the coverage but I don't trust that my code works.

Let's do a real test, where I start with an empty list of gnomes, then receive it, as a normal flow, then we take the input, mock the onChange function with different inputs and verify that the state changes correctly.

it('should render dashboard panel',()=> {
        // Mount Dashboard with none list of gnomes because the normal is that the first time never receive anything because the server was no called yet.
        const dashboardPanel = mount(<DashboardPanel gnomes={[]}/>);
        expect(dashboardPanel.find(GnomeBox).length).toEqual(0);

        // Mock the response of the server with 10 gnomes, the component will receive these props and validate that the 10 GnomeBox components are rendered.
        dashboardPanel.setProps(props);
        expect(dashboardPanel.find(GnomeBox).length).toEqual(10);

        //Find the filter component.
        const input = dashboardPanel.find(Input);

                // We mock the user iteration and send to the input an valid change event, and also we validate that the state change accordely, the filter text in the state  and is only one GnomeBox displayed.
        input.at(0).props().onChange({ target: { value: 'Tobus'}});
        expect(dashboardPanel.state('filterText')).toEqual('Tobus');
        dashboardPanel.update();
        expect(dashboardPanel.find(GnomeBox).length).toEqual(1);  

                // Then we validate the case where I just pass a letter and when we reset the filter to nothing again.
        input.at(0).props().onChange({ target: { value: 'a'}});
        expect(dashboardPanel.state('filterText')).toEqual('a');
        dashboardPanel.update();
        expect(dashboardPanel.find(GnomeBox).length).toEqual(4); 

        input.at(0).props().onChange({ target: { value: ''}});
        expect(dashboardPanel.state('filterText')).toEqual('');
        dashboardPanel.update();
        expect(dashboardPanel.find(GnomeBox).length).toEqual(10); 
    });
Enter fullscreen mode Exit fullscreen mode

Now let's see the status again:
Status after my real test

Everything is 100% of coverage but more importantly, I test all the possible behavior of my component, as a normal user will use it. Now I can trust that if everyone modifies the code, and the base behavior changes my test will catch it.

What we need to understand is that the coverage is just a number, the real coverage is what we need to get not just pass a random number. A component can have more complex behaviors, but in the end, what we need to do is understand the lifecycles and play with it.

Testing the middlewares

Today the react applications are becoming bigger and bigger and we now need to include extra logic on our App, sometimes we include middlewares to handle transactions that we don't wanna (and we shouldn't) include in our component, for this we can use redux-thunk, sagas or whatever. I'm going to explain to you how to test sagas, but this works with any Generator function

Let's check my saga file called gnomes, you can find it in the saga folder. I have 2 functions, but let's test it the first one, fetchGnomesSaga that is the one on charge to fetch the gnomes from the server, and looks like this:

export function* fetchGnomesSaga(option) {
    yield put(isLoading(true));
    const result = yield call(fetchGnomes, option);
    yield put(isLoading(false));
    if(!result.error) {
        yield put(gnomesFetched(result));
    }
}
Enter fullscreen mode Exit fullscreen mode

We need to have a test that calls this function and mocks the behavior of the transaction, sends the answers and validates that's correct. Let's start with a list with the base concepts of a generator function.

  • A generator is a javascript function, that is identify with the asterisk after the name like this function* fetchGnomesSaga(option) who will execute the code but will stop in each yield until gets an answer.
  • The yield is our transactions steps.
  • We need to validate each possible transactions responses based on what can be received on each step.
import { fetchGnomesSaga } from './gnomes';

describe('Saga Gnome test', ()=> {
    it('should fetch the gnomes correctly',()=> {
                // Set the Generator function in a constant
        const generator = fetchGnomesSaga({}); // We send nothing because we don't care this right now
        const isLoading = generator.next(); // The first stop is when the saga change the state to Loading
        expect(isLoading.value).toEqual(
            {'@@redux-saga/IO': true, 'PUT': {'action': {'payload': true, 'type': 'IS_LOADING'}, 'channel': null}}
        ); // Now we validate that the state is the correct.
    });
});
Enter fullscreen mode Exit fullscreen mode

In our test, the generator start and stop on the first yield, the one that will change the Loading status of the application, then, I call the generator.next() function to mock the response from redux indicating that the action was done, I can pass values on the next to indicate that the action sends some params, in this case, the redux just make the change on the state, doesn't return anything, that's why is empty.

To complete an Ok journey, we need to complete all the yields, like this:

it('should fetch the gnomes correctly',()=> {
        // Set the Generator function in a constant
        const generator = fetchGnomesSaga({}); // We send nothing because we don't care this right now
        let isLoading = generator.next(); // The first stop is when the saga change the state to Loading
        expect(isLoading.value).toEqual(
            {'@@redux-saga/IO': true, 'PUT': {'action': {'payload': true, 'type': 'IS_LOADING'}, 'channel': null}}
        ); // Now we validate that the state is the correct.

        // The next stop is the fetchGnomes API
        const callGnomes = generator.next();
        expect(callGnomes.value.CALL.fn).toEqual(fetchGnomes);

        // The next stop before receive the gnomes is disable the loading, in this step is where the data is received, so we send the data on the next
        isLoading = generator.next({ status: true, data: [1,2,3]});
        expect(isLoading.value).toEqual(
            {'@@redux-saga/IO': true, 'PUT': {'action': {'payload': false, 'type': 'IS_LOADING'}, 'channel': null}}
        );

        // We received the data already, but now we call the redux action who change the state with the payload received [1,2,3]
        const gnomesReceived = generator.next();
        expect(gnomesReceived.value).toEqual(
            {'@@redux-saga/IO': true, 'PUT': {'action': {'payload': {'data': [1,2,3], 'status': true}, 'type': 'GNOMES_FETCHED'}, 'channel': null}}
        );

        // The next step and the last one has just finished the generator, we need to validate it to avoid extra steps before the end.
        const endGenerator = generator.next();
        expect(endGenerator).toEqual({'done': true, 'value': undefined});
    });
Enter fullscreen mode Exit fullscreen mode

In the test above, you can see that I simulate the transaction to be a happy path, if someone modifies the code, and include extra steps that modify the result, I should be able to catch it too.

Let's see now how to handle a no happy path when the API returns an error:

it('should fetch the gnomes but fails ', ()=> {
        // Set the Generator function in a constant
        const generator = fetchGnomesSaga({}); // We send nothing because we don't care this right now
        let isLoading = generator.next(); // The first stop is when the saga change the state to Loading
        expect(isLoading.value).toEqual(
            {'@@redux-saga/IO': true, 'PUT': {'action': {'payload': true, 'type': 'IS_LOADING'}, 'channel': null}}
        ); // Now we validate that the state is the correct.

        // The next stop is the fetchGnomes API
        const callGnomes = generator.next();
        expect(callGnomes.value.CALL.fn).toEqual(fetchGnomes);

        // The next stop before receive the gnomes is disable the loading, here the fetch fails, we don't care the error, but we need to hanlde it.
        isLoading = generator.next({ error: true });
        expect(isLoading.value).toEqual(
            {'@@redux-saga/IO': true, 'PUT': {'action': {'payload': false, 'type': 'IS_LOADING'}, 'channel': null}}
        );

        // We received the data already, but now we call the redux action who change the state with the payload received [1,2,3]
        const gnomesNotReceivedAndDone = generator.next();
        expect(gnomesNotReceivedAndDone).toEqual({'done': true, 'value': undefined});
    });
Enter fullscreen mode Exit fullscreen mode

I basically change the API function to receive an error, when there's an error my code just doesn't update the gnomes state. If I have more than one call to any server, parser logic, or any other scenario, I should include a test to validate each scenario assuming that at any moment something can fail, if we code thinking that our code is fragile, we will be able to understand and prevent problems on the future, this's the reason behind why I should have so many tests.

Conclusion

So, we should test everything?
I tried to explain 2 common places on our react applications where we can have a lot of logic, components and the middleware, but we should not test everything just because is the rule, we should test everything that handles logic that affects our business scenarios.

the coverage it's a lie?
No, but trust only in the coverage without check the quality of the tests is the same that don't do any test. Part of a code review should include verifying that the test is ok for what's intended to test, a high coverage means test but is in the little details where our code fails, and the coverage doesn't say is we're ok on that sense.

Should I use jest only?
Jest is very powerful but is not the only one, you can use chai, mocha or any other framework, the library is just a help, in our first example we don't use any framework, the quality on a test is not for the tool used, is the test itself who can assure that.

How to create a culture of test on my team?
Is hard, because no one likes to write test, but teaching how to test, and explaining the value should be the first step.

How to make better tests?
Write better code, if you apply good patterns and principles, the test should be simple, if we realize that a test takes so much of our time, and is overly complicated, maybe the problem is our code and no the test itself. Divide and Conquer

Should I mock?
Depends on what you are trying to test, in you try to test a code that consumes multiple services, the best is just mock that services, replicating the real responses. A unit test should be isolated.

Well, I hope this post helps you to understand a little more about the testing process, not just how to test but why. Hope you enjoy the read.

In the code we trust

Check the second part of this post:

Check my previous posts

Discussion (3)

pic
Editor guide
Collapse
muthomimate profile image
Muthomi Mate

nice article though I think you meant shallow and not shadow

Collapse
comfortmarcapo profile image
comfort-marcapo

I enjoyed the read! Thanks :D

Collapse
mangel0111 profile image