How To Build Testable and Framework Agnostic Single Page Applications with Node.js

In a previous blog post, I spoke about the package principles of cohesion:

  • The Release/Reuse Equivalency Principle
  • The Common Closure Principle
  • The Common Reuse Principle

In this blog post, I will use the principles of package cohesion to build cohesive npm packages, and I will use these npm packages to build testable and framework agnostic single page applications.

For this demonstration, we will create a fake analytics dashboard. Here is what the final product will look like.

Screen Shot 2016-01-31 at 10.14.36 PMGathering Requirements

We will describe the application requirements using the “User Story Mapping” approach.

The following snippet describes the feature of our application

We want our “system under test” to be testable; so, we need to place some architectural restrictions to guarantee “testability”.

At a minimum, a testable system must have the qualities of (a) observability, and (b) controllability.

Observability refers to the property that I can arbitrarily observe the states of the “system under test”, and controllability refers to the property that I can put the “system under test” into an arbitrary state.

The two properties are inherently related to each other: you can’t fully observe a “system under test” that you can’t fully control, and there we don’t care to fully control a “system under test” that we can’t fully observe.

I claim (but will not prove) that these architectural restrictions guarantee “testability”.

In order to support the “controllability” requirement of our architecture, we will use the following background block to load fake data into our “system under test” for every new test run.

 

By design, a user story will have various scenarios, and we will use the scenarios to determine what needs to be “observable”.

For this demonstration we will consider the following scenarios:

  • Visit the visualization page
  • Change the selected date on the page
  • Aggregate the items by month
  • Aggregate items by year

This snippet shows the scenario description for “visit visualization page”

This snippet shows the scenario description for “change the selected date on the page”:

This snippet shows the scenario description for “aggregate the items by month”:

This snippet shows the scenario description for “aggregate items by year”:

You can see the complete feature file here.

Testing the User Story

The following code snippets uses cucumber.js and babel (an es6 to es5 transpiler) to test the user stories. I use babel because it lets me use generators and coroutines to simplify the asynchronous nature of selenium code.

This snippet initializes the “system under test” based on the data provided in the background block for the user story. This satisfies the “controllability” property of our architecture.

This snippet executes the action(s) that change the state of the “system under test” for the behavior “When I visit the page at <some_url>”:

This snippet executes the action(s) that change the state of the “system under test” for the behavior “When I select the date <some_date>”:

This snippet executes the actions(s) that change the state of the “system under test” for the behavior “When I select to aggregate by <some_aggregation_type>”:

This snippet executes the post conditions checks of the “system under test” for the block “Then I should see a bar chart for <some_title>”.

Note: I will skip the rest of the blocks for brevity’s sake.

In the acceptance tests, I use a page object to encapsulate the data retrieval algorithms for each page.

For example, the following code snippet shows how the page object encapsulates the algorithm that “observes” when the page has finished loading.

This test looks for a DOM element with the html attribute “qa-chart-type”, and then waits for it to change.

We have to do this because the SPA fetches data using a http request when if first loads, and we want to detect when the SPA receives a response.

However, since we cannot easily detect when the SPA receives an http response, we make the SPA communicate this to our test script by creating and updating special “Quality Assurance” html attributes.

This makes testing with selenium much easier and maintainable.

I also created a page object factory to easily change implementations of the page object. It uses the same principles of a “contract test” that I mentioned in my blog post “Contract Tests: or, How I Learned to Stop Worrying and Love the Liskov Substitution Principle”.  

Let’s suppose that we implemented our application using (a) Backbone.js, and (b) React.js.

In order to test a Backbone implementation, we could use the following code

In order to test a React implementation, we could use the following code

This is what running the cucumber application looks like when I test a React implementation:

screengrab

You can see the complete test suite here

Building Our Components

Suppose that you had the following directory structure for a JAVA application

  • src/
    • use_case001/
      • command/
        • File001.java
        • File002.java
        • File003.java
      • delegate/
        • File001.java
        • File002.java
      • model/
        • File001.java
        • File002.java
    • use_case002/
      • File001.java
      • File002.java
    • use_case003/
      • File001.java
      • File002.java
      • File003.java
    • controller/
      • Controller001.java
      • Controller002.java
    • view/
      • View001.jsp
      • View002.jsp
    • model/
      • Model001.java
      • Model002.java

This is an example of a “monolithic” or “big ball of mud” application architecture.

Architects typically have nothing but contempt for this architecture due to its well known rigidity, fragility, immobility, and viscosity.

These are well known application level architectural properties:

  • rigidity measures how easy an architecture can respond to change.
  • fragility measure how likely something will break when making a change
  • mobility measures how easy an architecture can support moving code
  • viscosity measure how easy an architecture supports maintaining the original design

When faced with a monolithic architecture, most architects I know would refactor the monolith into something that looks like this.

  • src/
    • controller/
      • Controller001.java
      • Controller002.java
    • view/
      • View001.jsp
      • View002.jsp
    • model/
      • Model001.java
      • Model002.java
  • lib/
    • use_case001.jar
    • use_case002.jar
    • use_case003.jar

Each .jar file is a self contained components that is independently testable, releasable, and versioned.

You might think that simply moving code from a directory to a .jar file does not significantly improve the design. However, when applied properly, it has a huge influence on reducing rigidity, fragility, viscosity, and increasing mobility.

We will use this approach, but in this case we will use npm modules as components: a npm module is equivalent to a JAVA .jar file; so, all the same rules apply.

I will use the discipline of Responsibility Driven Design to guide the cohesion strategy for our components. Responsibility Driven Design makes component “responsibility” the most important organizing principle for code.

I claim (but will not prove) that organizing our components by “responsibility” will force our components to obey (a) the Reuse-Release Equivalency Principle, (b) the Common Closure Principle, and (c) the Common Reuse Principle.

From the scenarios in the user story, I can identify the following responsibilities:

  • Bar Chart Visualization
  • User Interface Interaction
  • Data Retrieval
  • QA HTML Attribute Generation

Therefore, I will respectively create the four following packages:

  • analytics-chart
  • analytics-facade
  • analytics-service
  • qa-locator-utility

Create the “Analytics Chart” Component

The actual details of how I created the component are not important; so, I will not talk about it.

However, I feel that I should mention the unit tests I wrote for the visualizations because it isn’t a common thing.

I created unit tests to prove partial correctness of my application using the approach I describe in my previous blog post.

For example, the following code snippet tests the visualization for some items.

This test verifies that certain DOM elements appear in the svg canvas. I consider it a very weak test because there is a good chance that it could pass and the visualization could still be wrong. However, it is also a cheap test to write; so, there is still value in writing it.

You can see the completed npm package here.

Create the “Analytics Facade” Component

This component encapsulates all the user interface “glue logic” (i.e. identifying which screen or page to display), and we use the command pattern and delegation pattern to promote loose coupling between the user interface and this component.

For example, these are the cucumber features that I wrote for this component.

From a client’s perspective, they only need to know how to execute a command from the facade, and how to delegate tasks to the facade. The cucumber tests above provide a very high level description of what types of commands and delegates this facade exposes.

For the sake of completeness, I will also show you the step definitions for this feature.

You can see the completed npm package here.

Create the “Analytics Service” Component

This component simply makes a request to our remote server and return the json results. It isn’t very interesting; so, I will not discuss it.

You can see the completed npm package here.

Create the “QA Locator Utility” Component

Officially, we do not need this component, but we would like to have an easy method to locate elements in our window’s DOM using xpath. This component encapsulates any algorithms that we might need to simplify the process of creating html attributes for the purpose of writing acceptance tests.

I don’t consider it a very interesting component; so, I will not discuss it. 

You can see the completed npm package here.

Composing the Components Into A Single Page Application Framework

To demonstrate how to migrate the application between frameworks, I will compose the components into (a) Backbone 1.2, and (b) React 0.14.

The following component diagram shows the general relationships between the components and framework.

Screen Shot 2016-02-03 at 11.27.24 PM

Backbone 1.2 and React 0.14

We have placed the bulk of the applications functionality inside of framework independent components. Therefore, in order to create a backbone version or a react version of the application, we have to create a special component that coordinates between our “independent” components and the framework.

In the case of React, we created the component backbone_app.

In the case of Backbone, we create the component react_app.

For example, here is the entry point for our Backbone application:

Here is the entry point for our React application:

The parallels in the code are very apparent. I will not go into detail into the code bases because it is not important. I only care about the “shape” of the design.

You can find the Backbone implementation here, and the React implementation here if you really care to dig into the code, however.

Conclusion

This approach might seem very complicated for such a simple application … and it is.

In reality, this approach only makes sense once your application reaches a certain scale. However, simple applications have a tendency of becoming big applications, and it is very expensive to put a test harness on an already existing system with no tests.

Personally, I recommend you always build an application with decoupled testable components from the very beginning because that investment will pay large dividends in the long run.

Further, decoupling your components from the framework has huge benefits even if you have no intention of switching frameworks.

One practical “use case” is with automated testing.

When you decouple your components from the framework then you can test the bulk of your application independent of the framework. This significantly simplifies the overall testing process.

In fact, the package principles were partially created to minimize the work of creating and maintaining tests.

In a future post, I will show you how you can use a Continuous Integration tool to automatically run your tests whenever someone releases a new version of a component.

That should demonstrate the practicality of strictly following package principles.

Advertisements