JavaScript

A JavaScript Unit Test Trap

You’re a few months into writing a shiny new web app, and the team just settled on some new standards for data structures used for communication with the server.

You’ll need to refactor a few pages, but that’s not a big deal. Your team has been pushing good unit testing practices, and you’ve got great coverage for the affected code. You make the changes, verify that the unit tests still pass, maybe do a little manual testing for good measure, and then get everything committed.

A few days later, the bug reports start rolling in. This column no longer shows the right data. When I run Test Case 5 the Widget Editor doesn’t render correctly. The sort order for data on the home page is wrong.

They’re all for pages you modified, and nobody else has worked on those pages. You know your refactoring is to blame; but the unit tests were still passing! What the heck happened…?

Where Do Mock Objects Come From?

In a strongly-typed language like Java, you probably rely heavily on a mock object framework. Through the magic of gclib the framework dynamically creates subclasses of your various services, repositories, etc. and serves up instances of those subclasses to replace each dependency used by your subject code.

In JavaScript you don’t need all that. In fact, you can’t have all that, because nowhere do you definitively describe a “class” for your objects. So your unit testing tool may provide some mocking support (like the “spy” functionality in Jasmine), but in general your unit tests can just build objects with whatever members they expect the subject code to access.

Do you see the trap yet?

An Example in Two Languages

Your app handles orders. An order has a list of line items (each with a part number, quantity, and unit cost), a shipping address, and a shipping fee. To simplify the example, all costs are in whole dollars.

In Java you define the following classes:

public class Order {
	private List<LineItem> lineItems;
	private Address shipTo;
	private int shippingFee;

	// getters and setters ...
}

public class LineItem {
	private string partNumber;
	private int quantity;
	private int unitCost;

	// getters and setters ...
}

You also have an OrderService with the following method:

public int calculateTotalCost(Order order) {
	int result = order.getShippingFee();
	for (LineItem lineItem : order.getLineItems()) {
		result += lineItem.quantity * lineItem.unitCost;
	}

	return result;
}

To test the OrderService, you’ll want to pass in Order instances with known cost values and check that the correct total is returned. A pure approach would use mock objects with scripted responses for the getters, but for simple data objects with “logic-less” getters and setters, you might elect to create real Order objects instead.

(If the test needed to verify that specific Order methods were used along the way, we’d have to use a mock regardless; but we just want to test for the correct result.)

Order order = new Order();
List<LineItem> lineItems = new ArrayList<LineItem>();
LineItem item1 = new LineItem();

item1.setQuantity(2);
item1.setUnitCost(42);
lineItems.add(item1);
order.setLineItems(lineItems);
order.setShippingFee(37);

int result = orderService.calculateTotalCost(order);
assertEquals(121, result);

Meanwhile, another team is putting together a proof-of-concept for a more JavaScript-centric architecture. Your Order class gets JSON bindings; the above test Order, for example, would serialize as:

{ "lineItems": [
    "partNumber": "XL123"
  , "quantity": 2
  , "unitCost": 42
  ]
, "shipTo": { /*...*/ }
, "shippingFee": 37
}

They have an orderService.js containing a JavaScript version of calculateTotalCost():

var calculateTotalCost = function (order) {
	var result = order.shippingFee;
	for (var i = 0; i < order.lineItems.length; i++) {
		var lineItem = order.lineItems[i];
		result += lineItem.quantity * lineItem.unitCost;
	}

	return result;
};

They unit test their JavaScript code with Jasmine. If they need to pass in a simple data object, they build it on the spot. (Alternately, they might include helper functions in their test scripts to build these sorts of object.)

describe("calculateTotalCost()", function () {
	it("gives the total cost including shipping", function () { 
		var order = {
			  orderItems: [{
				  quantity: 2
				, unitCost: 42
			  }]
			, shippingFee: 37
		};
		var result = calculateTotalCost(order);
		expect(result).toEqual(121);
	});
});

Now while the proof-of-concept is being evaluated, this logic has to be maintained in two places; but both teams agree that the test coverage is good (meaning, presumably, that there are more tests than we’ve shown here), so nobody’s too worried.

The Trap Is Sprung

Mid-way through the proof of concept period, you get a new requirement: Shipping costs need to be attributed to specific line items. The JavaScript team is off site, but you figure they can catch up; worst case their unit tests will alert them that something’s broken, right?

The Java team agrees that to avoid potential inconsistency, the shippingFee property will be removed from Order. There was some talk about minimizing interface changes by keeping the Order.getShippingFee() method (rewriting it to sum up the line items’ shipping fee values), but everyone agrees that the order-level shipping fee should be removed from the JSON bindings in any case.

You update the Java classes. The unit tests don’t build since they reference nonexistent methods, so you fix them. Then you notice something weird…

The unit tests are passing. All of them. Even the JavaScript ones.

The problem is that the JavaScript unit tests are making the same wrong assumptions about the Order structure as the code they’re meant to test. In this case, it would likely get caught through even moderately good team communication; but in a bigger system, it’s possible some code would slip through the cracks.

And anyway, isn’t the point of unit tests to alert you to a defect even if you somehow miss it?

So Where Should Mock Objects Come From

There are two ways to improve on this situation. Ideally we’d like to remove assumptions about object structure from the unit test code. If we can’t do that, maybe we can figure out how to validate those assumptions.

In Java, our unit tests validate their assumptions about the structure of an order by trying to compile (and/or run) against the Order and LineItem classes. In JavaScript, though, an object doesn’t have a class.

I noted above that the unit test scripts could contain helper functions for building data objects. We could make it a rule that all data objects must be obtained from helpers, and that the helpers must all be in a central library shared by all test scripts. Then updating that central library should cause all affected tests to fail. This would tend to isolate the assumptions in the test code. It does have its limitations. Ultimately our test codebase still contains unverified assumptions about the object structure, and there’s no guarantee that each assumption can be isolated to a single helper function.

Another option is to create an Order constructor. Of course, when we receive a JSON representation of an Order, it won’t have gone through that constructor; so to make this meaningful, we have to use the received JSON to construct a “real” Order object that our code will operate on, and then throw the JSON representation away.

At a glance, this feels wasteful; but then at some point in history many of the conventions we use to promote testability and maintainability were received with the same criticism. As a side benefit, our constructed object could use closures to conceal the internal state and provide access through getters and setters, providing some of the encapsulation we had in the Java world.

When we first got the new requirement that changed our definition of an Order, we’d update the Order constructor unit tests accordingly, which would lead us to fix the Order constructor itself. Once the constructor is fixed, our unit tests would fail when trying to call setShippingFee() on the order, much like the Java case, and we’d have test failures pointing to each bit of code we need to fix.

Final Thoughts

This is likely not the only solution, and it may not be the best one; let us know how you’ve addressed this issue in the comments below.

Regardless, this should serve as a reminder that languages like JavaScript give us more than enough rope to hang ourselves. As the scale of your JavaScript projects increases, it becomes more important to self-impose some of the discipline that isn’t enforced by the language.

Reference: A JavaScript Unit Test Trap from our WCG partner Keyhole Software at the Keyhole Software blog.

Keyhole Software

Keyhole is a midwest-based consulting firm with a tight-knit technical team. We work primarily with Java, JavaScript and .NET technologies, specializing in application development. We love the challenge that comes in consulting and blog often regarding some of the technical situations and technologies we face.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button