-
Notifications
You must be signed in to change notification settings - Fork 36
Testing React components
Whereas properly designed presenter components are very easy to create business-level acceptance tests for (since they have a logical model of the view that can be tested against), React components have no in-built mechanism to make testing easier. React's in-built React.addons.TestUtils
class provides helper methods for testing against the virtual DOM, but not a logical view of the component.
The Jest library, that Facebook recommend for testing React components, is essentially Jasmine with some added smarts, but is inappropriate for a number of reasons:
- It's not BDD 'out of the box', so inferior to what we currently have in this regard.
- It uses CommonJS's
require()
as a hook to do dependency-injection, so that mock dependencies can be used when running tests, so is at odds with aliasing, which solves the same problem. - It uses automatic mocking to create items with a similar shape for all required dependencies, which has a number of issues:
1. Effectively forces unit testing of private implementation by assuming that all required dependencies are external things that should be mocked -- this is a particular problem for bag-of-class style libraries.
2. Incorrectly guesses which parts of the application we have well defined contractual relationships with, instead of requiring these to be explicitly defined as 'services', etc.
3. The mocks it produces may fail the
topiarist.isA()
check? - While problems #2 and #3 can be solved using
jest.autoMockOff()
, it's then not clear what gain over just using Jasmine.
Instead, I'd like to recommend the testing mechanism employed for BRJS as being a good candidate for doing business level tests, without the inconvenience of verifier-style fixtures, and without framework code obscuring what the tests are actually doing.
Here's an example BRJS spec test:
@Test
public void weBundleAnAspectClassIfItIsReferredToInTheIndexPage() throws Exception {
given(aspect).hasClass("appns/Class1")
.and(aspect).indexPageRefersTo("appns.Class1");
when(aspect).requestReceivedInDev("js/dev/combined/bundle.js", response);
then(response).containsCommonJsClasses("appns.Class1");
}
Each line of the test has a noun (e.g. aspect
), a tense (e.g. given (past), when (present) or then (future)) and an action (e.g. hasClass("appns/Class1")
). There are a number of benefits to this arrangement:
- The content-assist provided by the IDE informs the developer what actions are available for a particular noun and tense.
- Control-clicking on an action takes the developer directly to the code to perform the action, and there is no intermediary framework code before the action actually takes place.
- There is no need for fixtures and fixture factories, and instead actions are made available for a particular noun by creating a
Builder
class (for given actions), aCommander
class (for when actions) or aVerifier
class (for then actions).
The only glue code / framework code necessary is for the given()
, when()
, then()
and and()
methods, which need to return the correct instance of Builder
, Commander
or Verifier
, depending on the type of the input parameter. There are a number of ways this might be done:
- We could have written this using a generic
Builder given(Object noun)
method, but this would have lost all type information, and prevented content-assist within the IDE, which we have found to be a major benefit. - We could have written
Builder<T> given(T noun)
, and if Java Generics were more powerful we might have been able to create specialized methods for different flavours ofBuilder<T>
. - We could have created a dynamic code generator that creates code containing a unique
given()
method for each specialization ofBuilder
available, but my past experience with projects that depend on parser generators to create some of their code leads me to believe this will cause as many or more problems than it solves. - We could write all of the glue code manually, including the multiple variations of
given()
,when()
,then()
andand()
, which is a trivial but boring task.
For the BladeRunnerJS Java tests, we're currently using the option 4.
With JavaScript, since there is no typing, it's not even possible to overload the same method (e.g. given()
) multiple times based on the various types that might be received, and we are instead forced to do that checking internally using an instanceof
check. This necessarily prevents us from having a rich content-assist experience, without even taking into account all the other problems of achieving this with JavaScript.
TypeScript however, does allow function overloading, but it does it the JavaScript way, where only one method using instanceof
is written, but where additional type declarations can proceed the method so that the compiler understands what type will come back based on the input types. This is perfect since it could potentially allow a build that doesn't rely on generated code to work, but where the build could generate the additional typing info to improve the IDE experience if desired.
You can comment on this wiki page using issue https://github.com/BladeRunnerJS/brjs/issues/876.