Skip to main content

Testing React components with Jasmine in Rails with the react-rails gem

So in my latest project, we are using Ruby on Rails and the react-rails gem to implement our React components.

While react-rails is fine for the most part (and for simple components), there is a severe limitation in that you cannot use ES6 modules (i.e. using the import and export classes) because of the way asset-pipeline works.

Asset-pipeline precompiles all your JS into one mega JS file and therefore doesn't allow you to call individual JavaScript components in separate files (because they don't pushed to the server). This is a severe problem which I hope gets fixed soon, but in the meantime don't hold your breath.

There are workarounds using Browserify and/or Webpack, but they are messy (Google them for more info).

In any case, react-rails does allow simple React components in Rails apps and it mounts them all in the window/global namespace, so you can use a good 75% of React within a Rails application.

So now, we come to testing...

Facebook uses a testing framework called Jest (https://facebook.github.io/jest/docs/tutorial-react.html). It looks pretty good, but unfortunately does require the use of the import/export syntax...

But hey, React components are just JavaScript right? That means you can test them with any library (like Jasmine https://github.com/jasmine/jasmine-gem). Right?

Well, they are JavaScript, but they are also kind of closed off and it can be hard to actually unit test and mock them. I created a React component called DailyQuiz and for testing I tried to simply instantiate it normally.

var dailyQuiz = new DailyQuiz();

I was then able to see all the attributes on dailyQuiz in the console as if it were a regular JS object.

Until that is, I ran into setState in the code. Unfortunately, it gave me some guff about

Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the  component.

So, basically, this means we can't treat a React component as a plain JS object because there is a lot happening behind the scenes. In a way this makes sense because React does a lot of calculations before outputting to the DOM, so these restrictions are probably needed.

So rather than trying to fake the whole mounting of the component, I decided it would be better to actually mount it. I used jasmine-jquery (https://github.com/travisjeffery/jasmine-jquery-rails) to help me manipulate the DOM with jasmine.

I will show you the code snippet and then talk you through it.
  
  beforeEach(function() {
    deferred = new jQuery.Deferred();
    spyOn($, 'ajax').and.returnValue(deferred);
    setFixtures('[div id="react-fixture"]
test[/div]
');

    dailyQuiz = ReactDOM.render(
      React.createElement(DailyQuiz, {
        url: "/",
        saveUrl: "/save"
      }),
      document.getElementById('react-fixture')
    );

    deferred.resolve(data);
  });

* note for this example I had to replace the < and > with [ and ] for rendering purposes... Blogger is giving me some hassle...

So basically I am using $.ajax with the promise syntax and then spying on it so I can intercept the ajax call in my spec. I then use jasmine-jquery's setFixtures to create a place to mount the component, I mount the component with plain JS (not JSX) and the resolve the AJAX call.

From there, you can actually call dailyQuiz and it will reflect the state of the component.

The downside of this approach is that the data you use to populate your component has to actually work (you can't just stub any old data through it). There may be a way to spy on the subcomponents, but I haven't found a way yet.

The benefits are that you can test the actual state of the actual component. It's just not a headless component (it needs to be mounted). You can also use jasmine-jquery to test the DOM meets expectations.

Comments

Popular posts from this blog

Master of my domain

Hi All, I just got myself a new domain ( http://www.skuunk.com ). The reason is that Blogspot.com is offering cheap domain via GoDaddy.com and I thought after having this nickname for nigh on 10 years it was time to buy the domain before someone else did (also I read somewhere that using blogspot.com in your domain is the equivalent of an aol.com or hotmail.com email address...shudder...). Of course I forgot that I would have to re-register my blog everywhere (which is taking ages) not to mention set up all my stats stuff again. *sigh*. It's a blogger's life... In any case, don't forget to bookmark the new address and to vote me up on Technorati !

Elixir - destructuring, function overloading and pattern matching

Why am I covering 3 Elixir topics at once? Well, perhaps it is to show you how the three are used together. Individually, any of these 3 are interesting, but combined, they provide you with a means of essentially getting rid of conditionals and spaghetti logic. Consider the following function. def greet_beatle(person) do case person.first_name do "John" -> "Hello John." "Paul" -> "Good day Paul." "George" -> "Georgie boy, how you doing?" "Ringo" -> "What a drummer!" _-> "You are not a Beatle, #{person.first_name}" end end Yeah, it basically works, but there is a big old case statement in there. If you wanted to do something more as well depending on the person, you could easily end up with some spaghetti logic. Let's see how we can simplify this a little. def greet_beatle(%{first_name: first_name}) do case first_name d

Speeding up RSpec

So today I have been looking into getting our enormous battery of tests to run faster. I have yet to find anything that works for Cucumber, but I did find an interesting way to speed up RSpec which is detailed here. https://makandracards.com/makandra/950-speed-up-rspec-by-deferring-garbage-collection Basically, it seems that by not collecting garbage too frequently, you can make your tests run much faster (at the expense of memory management of course). We observed a 30% reduction in the time it takes to run an RSpec test suite. I did try to implement this on Cucumber, however because we need to store much more in memory to set up and tear down our objects, it meant that I kept running out of memory when I wasn't using the default Garbage Collection and the tests took even longer (so, buyer beware). I suppose if you had a small set of features though you might see some benefit.