Monday, January 16, 2017

JS Testing In Rails Apps: The Problem Space

I've got to do several things before my new book, Modern Front-End Development with Ruby on Rails and Puppies, is finished. It's currently only in version 0.2! One of the biggest things is to delve into JS testing in serious depth. The "with Puppies" part of my book's title doesn't add any extra challenges to that task, but the "with Ruby on Rails" part totally does.

The Rails culture has very high standards and expectations when it comes to the sophistication and specificity of its testing options. Say you've got a big Rails app which has presenters, decorators, and serializers. That's three different categories of view model objects — and standard methods of testing these objects exist both for RSpec and for classic TDD syntax (i.e., the assert style of Test::Unit and minitest). So you can go out and find, for example, specific, opinionated guidance on how to write tests for presenters using minitest. And I've obviously defined a 3x2 grid here, with 3 categories of view model object, and 2 categories of testing strategy. All six slots in the grid represent a strain of thinking within the Rails culture that lots of people are working on in detail. If you want to research any one of those six slots, there's a ton of prior art.

And that's just for view model objects. What about objects which move business logic out of the framework? Those can go in lib, or app/services, or app/interactors, or several other places. Want to test those quickly and well? There are projects which give you several different ways to do it. If you see open source development as a form of research, then you have this huge body of ongoing research. All these different strategies for separating business logic from the framework, and all the different ways of testing the objects which implement these different strategies — they all represent ongoing research projects into the best way to structure an application, and the best way to design an application, and/or secure that application, and/or prove its correctness (depending on how you see the purpose of testing in the first place, which itself is an area of ongoing "research," in this sense).

The common structure of Rails apps creates a shared vocabulary, not just for application logic, but for testing as well. You can have subtle, specific discussions about how to compartmentalize, organize, and structure responsibilities and processes across Rails apps, even when those apps do very different things.

Compare that to JavaScript. How many JavaScript applications share a common structure? Most don't.

The size of the Ruby community also helps here, in comparison to JavaScript. "The JavaScript community" basically means the same thing as "every programmer on the planet who ever works on or near the web." In fact, it's larger than that; if you want to script Adobe applications, for example, you use JavaScript. So for Ruby, you have a small community, where the overwhelming majority of programmers work within the same application structure, and where virtually everybody agrees that testing is important. For JavaScript, you have a group of people which is far too large to ever function as an actual community. Most applications have relatively idiosyncratic structures, and many of them change radically from month to month. What kind of consensus can emerge there?

The good news is that there are plenty of JS programmers who value testing, and plenty of JS test frameworks, to enable that. But the bad news is that many of these testing systems never get much further than unit testing. There's no Capybara of JavaScript, for example — not really — which is pretty crazy, because front-end coding is the type of work where you would expect to find a Capybara being developed.

Say you've got a Rails app with a React front-end. Maybe you're using Rails controllers for the regular web stuff and Grape for the API stuff that your React code consumes. So you can put your Grape specs in spec/requests and write simpler specs, and keep your controller specs in the more usual spec/controllers, and deal with the unfortunate downsides, i.e., the slowness, the intricate connection(s) to the framework, et cetera. But when you want to test your JS, you don't get these kinds of fine-grained distinctions for free. You can use moxios to mock your axios requests, but that's kind of as sophisticated as it gets. You have unit testing, and you have mocks, and there you go. It's like that scene in The Blues Brothers, when they ask "what kind of music do you usually have here?" and the bartender says, "we got both kinds of music, country and western!"

With JS, there are many ways to do mocks and stubs, and there are many ways to do unit testing, but there's nothing as purpose-specific as spec/requests vs spec/controllers. You're also flying blind somewhat when it comes to distinctions as fine and precise as when to use a presenter, when to use a decorator, and when to use a serializer (which is basically just a presenter for APIs).

One of the benefits of something like React is that it's an improvement just to get a sub-community (or subculture) of JS devs who all use roughly the same application structure, because that common vocabulary permits the subculture to develop more sophisticated and specific testing libraries. As far as that goes, Angular and Vue and other frameworks have a similar positive influence. However, this is especially a strength of React in my opinion, and in fact, I need to update my book to reflect that. For some reason, when people talk about React, they talk a lot about FRP and virtual DOMs, but when you get past the hand-waving, the actual day-to-day work of React is very object-oriented. There are a lot of tradeoffs to consider with OOP, but one great positive is that OOP systems present very testable surfaces. It's really easy to see where you start testing, when you're dealing with objects.

Elm has this "common application structure" advantage as well, but with Elm, you have types, and types relieve a lot of the pressure on tests. That's not to say that you don't need tests when you have types. But since the compiler is guaranteeing you certain behavior, you don't need tests to secure that behavior. People use tests to design systems, to secure systems (and prevent regressions), to document systems, and for a few other purposes. Elm's types make it easy to get most of your security cheaply. Elm also pushes you towards better designs in ways that are more subtle and which I couldn't perfectly articulate just yet.

However, in both these cases, this isn't a solution, but rather a reason to be optimistic that the cacophony which has prevented solutions from arising will soon diminish, at least somewhat.

I'll have more to say about this in the future; for now, this is just an overview of how the problem space for JS testing in Rails apps differs quite a bit from Ruby testing in Rails apps.