Testing React withRouter

One area I’ve always found tricky when writing tests for React is where Higher Order Components are involved. I’ve found this complicates the test setup process. There are ways around this, which may or may not be possible depending on many factors. Sometimes you work on third party code that won’t accept ‘dramatic’ refactors just to scratch your own testing itches.

One example of where this problem might occur (particularly if you don’t read the docs!!) is with React Router, specifically when using withRouter.

const MyComponent = ({ history }) => { ... });

export default withRouter(MyComponent);

In this example I have MyComponent which wants to history.push('/some/location') when the user completes some action.

I’d like to test that this process takes place as expected.

Here was my first attempt. This way works, but there are some drawbacks:

import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';

// ...

  it('should redirect when on the happy path', () => {
    const history = createMemoryHistory({
      initialEntries: ['/starting/point']
    });

     const { getByLabelText } = render(
      <Router history={history}>
        <Provider>
          <MyComponent />
        </Provider>
      </Router>
    );

    expect(history.location.pathname).toEqual('/starting/point');

    const myInput = getByLabelText('Some label text');

    const value = { target: { value: 'a value here' } };

    fireEvent.change(myInput, value);

    fireEvent.keyPress(myInput, { key: 'Enter', code: 13, charCode: 13 });

    expect(history.location.pathname).toEqual(
      '/path/when/redirected'
    );
  });

This works.

There are a couple of drawbacks to this:

  • It involves a couple of extra imports.
  • Behind the scenes, withRouter is still used (afaik) but by wrapping in another Router, we can override the history prop.

As this particular approach is so common, React Router gives us an alternative / preferable way to test this workflowWrappedComponent.

  it('should redirect when on the happy path', () => {
    const history = { push: jest.fn() };

     const { getByLabelText } = render(
      <Provider>
        <MyComponent.WrappedComponent history={history} />
      </Provider>
    );

    const myInput = getByLabelText('Some label text');

    const value = { target: { value: 'a value here' } };

    fireEvent.change(myInput, value);

    fireEvent.keyPress(myInput, { key: 'Enter', code: 13, charCode: 13 });

    expect(history.push).toHaveBeenCalledWith(
      '/path/when/redirected'
    );
    expect(history.push).toHaveBeenCalledTimes(1);
  });

The necessary actions to ‘run’ this test are unchanged. However, there are fewer lines as we can make use of the existing constructs provided by React Router to aid our testing workflow.

This may have been extremely obvious to you.

I can’t remember if WrappedComponent has always been available and I have overlooked it, or it is something new since I last had to do testing with a project using React Router.

Either way, it’s time I refreshed my knowledge of the documentation. And hopefully this helps someone else when testing withRouter at some point in the future.

The Truth About Testing

In this week’s new videos we are diving head first into PhpSpec.

I am fully aware that this is a series that is aimed at beginners, but in my opinion it’s never too early to learn how to test.

Before we go further though, I do want to stress that PhpSpec is a highly opinionated approach to testing, and that opinion may not be one that you agree with. Or, more importantly, enjoy the feeling you get when you use it.

There are a couple of alternatives to PhpSpec that I have used and would recommend:

But this is somewhat misleading, as Codeception uses PhpUnit for unit testing.

Ok, so at this point, if you are someone new to testing, you may be feeling confused. A very valid question here is:

What the heck is unit testing?

My definition of unit testing is testing individual functions / methods.

There’s major benefits to doing this, which I am hoping to explore with you in these next few videos.

Let’s quickly cover unit testing by way of an example. Imagine we have a function that adds two numbers together:

function add($x, $y)
{
    return $x + $y;
}

We could manually test this:

echo add(1,2);

And we should expect to see 3.

One of the first things we do when writing code is find a way to output what we’ve done – to check if what we are doing is working. In PHP we can do this in a variety of ways: die , var_dump , echo , print_r , the list goes on.

This is a form of testing.

If all we are testing here is the output of a single function then heck, this is manual unit testing.

The problem is: we generally don’t have just one function. And if we do, that’s a different kind of problem in itself 🙂

Instead, what we will likely do is start using this add function in a variety of situations throughout our code base.

We might manually test those functions as well. In doing so, we indirectly test the other functions that function depends on.

And that’s all good.

Whilst all this logic is fresh in your head – whilst you’re deep into the systems internals – it’s all there as clear as day. But then another problem arises and you get sidetracked.

And when you return to this code even just a short time later, somehow the fog has begun to set in. You’re not quite sure how that method works anymore. You aren’t able to interpret that 70 line method exactly in your head.

At this point our untested code immediately becomes legacy code. At least, that’s my opinion.

We want to start refactoring it, to fix up all the confusion and restore that prior clarity.

But we can’t because the more we meddle, the more stuff breaks. Or may break. Coding is so stressful!!!!

zen-monk

Or, you can swap all this for zen.

You know your code works because the tests pass. Those tests didn’t pass to begin with. You’ve had to write code to prove how your system works. And now when you change your code, if your tests still pass then your system still works exactly as you intended.

And it’s surprising how, when doing this, you will break so many unexpected things when changing seemingly unrelated bits and pieces.

This takes your skill level further. You start to learn about better design as a result.

I’ve found PhpSpec will lead you towards a certain design.

That design is the distilled guidance from very clever people. Why not leverage their knowledge to help your project, but also you – to make your life easier, and less stressful?

Back to our function.

What if we had created our function using unit tests for guidance?

Well, we could take a look at using a unit test suite to write tests for this function.

However, the reality of it is, if you’re working with Symfony, you’re going to be writing code in a certain way.

Why not learn how to write unit tests in an environment just like your real world projects?

I think this is a better way to learn. It’s a little more effort upfront, but you’re learning how it can really and immediately help you become better on the type of code you have to write in your day job.

And so that’s what we are doing in these three videos:

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/testing-with-phpspec-to-guide-our-implementation

In case you haven’t been following along, in the past three video’s we’ve been learning how we can use EasyAdminBundle as a quick way to add a really nice UX to our admin area.

We can manage all our existing wallpapers from the back end, but it would be super useful to us if we could handle new wallpaper file uploads from the admin area, too.

EasyAdminBundle comes with a documented way to integrate with VichUploaderBundle.

We could have chosen to go that route, again leveraging the wisdom of the collective.

Instead we are doing some DIY. Our design decisions are not about sharing this code. We’re just exploring some concepts. We want to learn about file uploads, and hopefully improve ourselves as developers a little in the process.

The thing is, handling uploads is code that will be super important to the site and, if it all works, it will be used a lot.

This is the sort of problem that if you don’t handle up front, you’re going to be getting frantic calls from bosses and clients to fix as a matter of up-most urgency :/

No one wants to deal with that. Not you. Not your boss. Not the client.

So, when I hit on code like that, I reach for the unit tests.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/using-phpspec-to-test-our-filemover

In the previous video we covered a little of how the design decisions came about, and started our test routine.

Now we’re going to write some specifications of how we expect this system to behave, if it is performing as expected.

🙂

This is the insurance policy that allows us to refactor our confusing code later on.

If the implementation we write now makes the tests pass, we can know for sure if our code changes don’t lead to failing tests, we have altered the logic without altering the outcome.

If your project survives, this makes it’s lifetime more enjoyable for you as a developer.

You’ll inevitably get to work on more features, not constant and stressful bug fixing.

https://codereviewvideos.com/course/let-s-build-a-wallpaper-website-in-symfony-3/video/symfony-dependency-testing-with-phpspec

Finally we get into how all this integrates with Symfony.

Understand these three pieces and the process essentially repeats over and over for whatever you want or need to test.

The truth about testing is that it’s a pain to get started.

But once you have tested one thing, you can use that one single thing as a reference and it becomes A LOT easier to add tests to an already tested system.

Ok, so that’s why I love testing.

Hopefully if you can invest just 15 minutes into your PHP skills this weekend, it will be these minutes you choose 🙂

I’d love to hear your thoughts on this series so far.

I’m also under way with the big back end refresh once again. I’m going to be deploying this new site as the topic of the upcoming Docker series, which I think is quite a cool way to cover it. It will at least show you how it all works behinds the scenes, if nothing else.

If you haven’t done so recently, why not check out what else is on the site?

Thanks for reading, and have a great weekend,

Chris

 

7 things I learned adding Jest to my existing JavaScript boilerplate

Here are 7 things that I encountered when trying to add Facebook’s Jest to my existing React JS project.

This project was initially based on Corey House’s boilerplate – React Slingshot. There was nothing wrong with the test setup the boilerplate provided. I just wanted an excuse to try Jest.

1. Jest doesn’t play well with importing stylesheets

Here was the code I was using:

import React from 'react';
import '../styles/about-page.css';

const AboutPage = () => {
  return (

And the associated Jest output:

 FAIL  __tests__/components/AboutPage.react-test.js
  ● Test suite failed to run

    /home/chris/Development/react-registration-demo/src/styles/about-page.css:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){.alt-header {
                                                                                             ^
    SyntaxError: Unexpected token .
      
      at transformAndBuildScript (node_modules/jest-runtime/build/transform.js:284:10)
      at Object.<anonymous> (src/components/AboutPage.react.js:2:27)
      at Object.<anonymous> (__tests__/components/AboutPage.react-test.js:2:44)

To be fair, the output looks a bit nicer in the terminal.

Removing the stylesheet line:

import React from 'react';
// import '../styles/about-page.css';

const AboutPage = () => {
  return (

Now commented out, all passing:

 PASS  __tests__/components/AboutPage.react-test.js
  ✓ Can see header (10ms)

Snapshot Summary
 › 1 snapshot written in 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 added, 1 total
Time:        1.077s
Ran all test suites.

2. Tests live in the __tests__ directory

The tutorial link doesn’t make it immediately obvious that the location and naming of your test files has an assumptive default config:

(/__tests__/.*|\\.(test|spec))\\.(js|jsx)$)

The boiler plate I am using has the test files in the same directory as the component itself. I followed the boiler plate pattern first, and it didn’t work.

I was confused why my tests wouldn’t run:

> jest

No tests found
  50 files checked.
  testPathDirs: /home/chris/Development/react-registration-demo - 50 matches
  testRegex: (/__tests__/.*|\.(test|spec))\.jsx?$ - 0 matches
  testPathIgnorePatterns: /node_modules/ - 50 matches

Tests need to go in a __tests__ directory, unless you are a regex ninja.

If you do want to overwrite it then you can update your package.json with extra config:

  "jest": {
    "testRegex": "your regex here dot star hash"
  }

I went with their defaults.

Now also note, you wouldn’t have this problem if you had read the landing page and the Docs link. I made the mistake of going direct to the Docs link via Google.

3. You may need to rename things

In React so far I have seen files named like either thing.jsx,  or thing.js. I’ve never seen anyone use thing.react.js.

However, the Jest docs use this convention.

So, having made two mistakes already, I made the switch myself.

Tangible benefit: None(?)

4. Jest ran existing tests just fine

This one threw me.

I had an existing file left over from my purge of the boilerplate demo site. I manually deleted the files as I worked my way through and replaced the boilerplate parts with my own.

One such file that was left was a utility class called mathHelper.spec.js 

Jest ran this just fine:

> jest

 PASS  __tests__/components/AboutPage.react-test.js
 PASS  src/utils/mathHelper.spec.js

Test Suites: 2 passed, 2 total
Tests:       13 passed, 13 total
Snapshots:   1 passed, 1 total
Time:        1.157s

5. Snapshot Testing is a really smart idea

You are kidding me.

Another concept to learn?

When will this learning ever end? Never. Get reading.

Actually though this is fairly simple to understand, and incredibly beneficial.

An example illustrates it best.

Let’s say I have a very basic component:

import React from 'react';

const AboutPage = () => {
  return (
    <div>
      <h2 className="alt-header">About</h2>
      <p>Who wouldn't want to know more?</p>
    </div>
  );
};

export default AboutPage;

And a really simple test:

import React from 'react';
import AboutPage from '../../src/components/AboutPage.react';
import renderer from 'react-test-renderer';

test('Can see header', () => {
  const component = renderer.create(
    <AboutPage />
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

The first time I run the tests with npm test then Jest will create a snapshot of how this page is expected to look:

exports[`test Can see header 1`] = `
<div>
  <h2
    className="alt-header">
    About
  </h2>
  <p>
    Who wouldn\'t want to know more?
  </p>
</div>
`;

And the test output:

> jest

 PASS  __tests__/components/AboutPage.react-test.js
  ✓ Can see header (9ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        0.827s, estimated 1s
Ran all test suites.

Then, should you make a change to your component in some way which causes the output to change:

import React from 'react';

const AboutPage = () => {
  return (
    <div>
      <h2 className="alt-header">About</h2>
      <p>Changed</p>
    </div>
  );
};

export default AboutPage;

Then re-run:

> jest

 FAIL  __tests__/components/AboutPage.react-test.js
  ● Can see header

    expect(value).toMatchSnapshot()
    
    Received value does not match stored snapshot 1.
    
    - Snapshot
    + Received
    
      <div>
        <h2
          className="alt-header">
          About
        </h2>
        <p>
    -     Who wouldn't want to know more?
    +     Changed
        </p>
      </div>
      
      at Object.<anonymous> (__tests__/components/AboutPage.react-test.js:10:16)
      at process._tickCallback (internal/process/next_tick.js:103:7)

  ✕ Can see header (11ms)

Snapshot Summary
 › 1 snapshot test failed in 1 test suite. Inspect your code changes or run with `npm test -- -u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        1.078s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

Again, it looks nicer in the console.

6. You probably want an alias for updating snapshots

Here’s what I use, feel free to differ:

  "scripts": {
    "test": "jest",
    "testu": "npm run test -- -u",
  },

Every time you make a change to your component you likely need to re-update your snapshots. Having an alias is better than the alternative of typing out morse code.

7. There are concerns I only found out about after reading the Snapshot announcement blog post

Dropped in at the bottom of the blog post announcing Snapshot testing is some notes about forthcoming improvements.

This one caught my eye the most:

  • Mocking: The mocking system, especially around manual mocks, is not working well and is confusing. We hope to make it more strict and easier to understand.

Thankfully I did this migration on a new git branch 🙂

How I Fixed: TypeError: ‘null’ is not an object (evaluating ‘currentSpec.$injector’)

 

I hit upon a problem in testing an Angular project this week that had me stumped for a while. The problem was this:

Every time I ran the test suite as a whole, they failed.

But if I ran each test file on its own… individually, they would pass.

I find more and more of my time lately is spent dealing with these sorts of things – they aren’t ‘development’ tasks, just annoyances that keep me from doing the thing I enjoy most – writing code.

Anyway, here’s the error:

INFO [PhantomJS 1.9.8 (Linux 0.0.0)]: Connected on socket DGb_Xn6WLWQNnEEkUwtk with id 45807911
PhantomJS 1.9.8 (Linux 0.0.0) LOG: 'no config'
 
PhantomJS 1.9.8 (Linux 0.0.0) LOG: 'no config'
 
PhantomJS 1.9.8 (Linux 0.0.0) LOG: 'no config'
 
PhantomJS 1.9.8 (Linux 0.0.0) UrlManager should have get function FAILED
        TypeError: 'null' is not an object (evaluating 'currentSpec.$injector')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23390:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2330:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'null' is not an object (evaluating 'currentSpec.$injector')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23390:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2330:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'null' is not an object (evaluating 'currentSpec.$injector')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23390:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2330:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'null' is not an object (evaluating 'currentSpec.$modules')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23514:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2454:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'undefined' is not an object (evaluating 'UrlManager.get')
            at /tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:42035:0 <- /my/project/src/components/UrlManager/spec/UrlManagerSpec.js:26:0
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
PhantomJS 1.9.8 (Linux 0.0.0) UrlManager should return object for the others FAILED
        TypeError: 'null' is not an object (evaluating 'currentSpec.$injector')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23390:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2330:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'null' is not an object (evaluating 'currentSpec.$injector')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23390:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2330:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'null' is not an object (evaluating 'currentSpec.$injector')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23390:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2330:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'null' is not an object (evaluating 'currentSpec.$modules')
            at workFn (/tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:23514:0 <- /my/project/node_modules/angular-mocks/angular-mocks.js:2454:0)
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
            at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
        TypeError: 'undefined' is not an object (evaluating 'UrlManager.get')
            at /tmp/7b8601aaed330a593345d271f7b468335eb33d59.browserify:42053:0 <- /my/project/src/components/UrlManager/spec/UrlManagerSpec.js:44:0
            at /my/project/node_modules/karma-jasmine/lib/boot.js:126
           at /my/project/node_modules/karma-jasmine/lib/adapter.js:171
            at http://localhost:9876/karma.js:182
            at http://localhost:9876/context.html:257
PhantomJS 1.9.8 (Linux 0.0.0): Executed 7 of 7 (2 FAILED) (0.415 secs / 0.026 secs)
 
=============================== Coverage summary ===============================
Statements   : 17.48% ( 482/2758 )
Branches     : 4.14% ( 40/967 )
Functions    : 4.65% ( 30/645 )
Lines        : 17.62% ( 477/2707 )
================================================================================
[13:41:23] 'test' errored after 8.21 s
[13:41:23] Error in plugin 'test'
Message:
    Karma test returned 1
blimpyboy@project-dev1:~/Development/project$ ^C
blimpyboy@project-dev1:~/Development/project$ ^C
blimpyboy@project-dev1:~/Development/project$ gulp test
[13:42:58] Warning: gulp version mismatch:
[13:42:58] Global gulp is 3.9.0
[13:42:58] Local gulp is 3.8.11
[13:42:59] Using gulpfile ~/Development/project/gulpfile.js
[13:42:59] Starting 'test'...
INFO [framework.browserify]: Paths to browserify
    /my/project/src/components/PubSub/spec/**/*.js
    /my/project/src/components/UrlManager/spec/**/*.js
INFO [framework.browserify]: Browserified in 6543ms, 6524kB
INFO [karma]: Karma v0.12.37 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Linux 0.0.0)]: Connected on socket VrUi5MBGOl0J1oHWVKNw with id 50398227
PhantomJS 1.9.8 (Linux 0.0.0) LOG: 'no config'
 
PhantomJS 1.9.8 (Linux 0.0.0) LOG: 'no config'
 
PhantomJS 1.9.8 (Linux 0.0.0) LOG: 'no config'
 
PhantomJS 1.9.8 (Linux 0.0.0): Executed 7 of 7 SUCCESS (0.056 secs / 0.035 secs)
 
=============================== Coverage summary ===============================
Statements   : 20.01% ( 552/2758 )
Branches     : 5.07% ( 49/967 )
Functions    : 6.98% ( 45/645 )
Lines        : 20.21% ( 547/2707 )
================================================================================
[13:43:07] Finished 'test' after 8.1 s
blimpyboy@project-dev1:~/Development/project$

I’m aware the coverage isn’t so good – but actually this is not the true coverage as I’d stripped out a whole bunch of tests by bastardising the karma.conf.js file to try and isolate the problem. No… seriously, I promise 🙂

Anyway, it turned out that the solution to this was actually pretty simple.

Of course, nearly all solutions to programming problem seem simple once you have figured out the problem. Hindsight is such a wonderful thing.

But in this case, the problem was that a bunch of variables had been declared inside on the of describe blocks:

describe("SomeModule module", function () {

    beforeEach(angular.mock.module('Some.Module'));

    var angular = require('angular');
    var scope;
    require('../../../scripts/app.js');
    require('angular-mocks');

    var SomeModule;

And the solution was to simply move all the setup stuff outside of the describe block:

var angular = require('angular');
var scope;
require('../../../scripts/app.js');
require('angular-mocks');

var SomeModule;

describe("SomeModule module", function () {

    beforeEach(angular.mock.module('Some.Module'));

An easy fix.

The real annoyance here was that I went through this whole project alphabetically, and this particular module began with the letter ‘P’, so I’d been through over half the code before I spotted it. Hours I will never get back.

Still, it’s fixed now, and hopefully now you can save a few hours if you ever suffer from this problem yourself.