Respect the Underscore

Steven Elberger
Ritual
Published in
7 min readMar 8, 2020

What you’re about to read is a painful lesson I had the misfortune of learning the hard way. I spent a lot of time and energy being taught this lesson so hopefully you won’t need to do the same!

Here at Ritual, we use Ember.js to build our account management and checkout pages. Ember.js is a solid and mature framework for building ambitious web applications, especially ones that have a lot of state management and data.

One important thing to note about web development is that things are always changing for the better. Frameworks are updated at lightning speeds and it’s important to always be working with the latest and greatest versions.

Ember.js is no exception and has been making strides in both performance optimizations as well as developer ergonomics. Recently, Ember released Ember Octane — a whole new suite of features that makes it even greater to work with and, in my mind, makes it one of the best frameworks out there today.

Several months ago, I was tasked with upgrading our checkout app from Ember version 2.18 to 3.13 which went smoothly thanks to Ember’s well-laid-out upgrade documentation. We were able to utilize new Ember features, update old syntax, and continue to crank out code. What I didn’t realize at the time was that this upgrade secretly broke something in our code base.

But life went on, features were developed, other bugs were squashed. Around this time, we were also trying out Percy.io for automated regression testing. It was unbelievably easy to get Percy up and running! All we needed to do was throw in one line in our tests to take a snapshot:

percySnapshot(“Some Component”);

This worked great! But it was expensive and we were only able to take snapshots of individual components in their integration tests. While it was awesome to see them isolated, it would have been better if we were able to see them in the context of the pages they were used on.

Enter: acceptance tests! Ember is a huge proponent of having good code coverage (i.e., testing your code!). It has 3 different types of tests you can write:

  • Unit tests — used for testing services, models, util functions, etc.
  • Integration tests — used for testing components, making sure they render correctly and testing any logic through the DOM
  • Acceptance tests — used for testing the application as a whole. Sort of an end to end test but just for your front end code. It builds the entire application!

Why not render a whole page through an acceptance test and take a Percy snapshot to save money? That’s exactly what I tried to do… and this is how the bug finally came to light.

I generated an acceptance test using Ember CLI:

ember g acceptance-test some-route

And to my surprise the generated acceptance test failed!

So I did what I always do: looked at the console errors and opened up the test in dev tools. These were the errors in the console (in this order):

Source:
TypeError: Cannot read property ‘lookup’ of undefined
at Object.initialize (http://localhost:4200/assets/my-app.js:10312:28)
at http://localhost:4200/assets/vendor.js:61627:21
at Vertices.each (http://localhost:4200/assets/vendor.js:80243:9)
at Vertices.walk (http://localhost:4200/assets/vendor.js:80157:12)
at DAG.each (http://localhost:4200/assets/vendor.js:80087:22)
at DAG.topsort (http://localhost:4200/assets/vendor.js:80095:12)
at Class._runInitializer (http://localhost:4200/assets/vendor.js:61653:13)
at Class.runInitializers (http://localhost:4200/assets/vendor.js:61625:12)
at Class._bootSync (http://localhost:4200/assets/vendor.js:59923:14)
at Class.boot (http://localhost:4200/assets/vendor.js:59890:14)
Source:
Error: Cannot call `visit` without having first called `setupApplicationContext`.
at visit (http://localhost:4200/assets/test-support.js:44177:13)
at Object._callee$ (http://localhost:4200/assets/tests.js:23:47)
at tryCatch (http://localhost:4200/assets/vendor.js:12365:40)
at Generator.invoke [as _invoke] (http://localhost:4200/assets/vendor.js:12591:22)
at Generator.prototype.<computed> [as next] (http://localhost:4200/assets/vendor.js:12417:21)
at asyncGeneratorStep (http://localhost:4200/assets/tests.js:6:105)
at _next (http://localhost:4200/assets/tests.js:8:196)
at http://localhost:4200/assets/tests.js:8:366
at new Promise (<anonymous>)
at Object.<anonymous> (http://localhost:4200/assets/tests.js:8:99)

At this point in time, I noticed there was definitely a problem with the call to `visit()`. The error message seemed pretty straightforward so I hoped it was going to be an easy fix!

After a little digging and debugging, I saw that something was setting the test context incorrectly. It was just an empty object:

Inside the call to `visit(“/some-route”)`, `isApplicationTestContext(context)` returned false and the second error from above got thrown. I guessed that the lookup error was just being thrown from our application initializers since our application was being built for the acceptance test but perhaps that was simply due to the application context. To me, the more obvious problem was the second error message. It was clear that particular function was not being called.

After some Googling, it seemed like the Ember upgrade did not update the `test-helper.js` file. Our file was outdated! Simple fix — just re-write the old file with the new way of doing things. I hoped this would do the trick.

The old `test-helper.js` file was like so:

import resolver from “./helpers/resolver”;
import { setResolver } from “@ember/test-helpers”;
import { start } from “ember-cli-qunit”;
setResolver(resolver);start();

I updated it to exactly what it should be:

import Application from “../app”;
import config from “../config/environment”;
import { setApplication } from “@ember/test-helpers”;
import { start } from “ember-qunit”;
setApplication(Application.create(config.APP));start();

Great! I ran the tests again. To my surprise, every single one of our tests was now failing… What??

I tried re-adding the `setResolver` call but it didn’t make a difference. I tried Googling about the test helper file and this context issue but all I could find was that maybe `autoboot` wasn’t being set to false in the environment file. Unfortunately, that didn’t make a difference either.

I left it alone for the rest of the day and came back to it the following day. What was I missing?

Here were the facts:

  • The call to `visit` complained about the context.
  • There was an error regarding a call to `lookup`.
  • The test helper file was now correct. It was the new way of doing things and it really shouldn’t be causing any problems.

It was difficult to accept that the test helper file was correct since fixing it broke every test in the suite. But where were the overlaps? Why would the entire test suite fail after fixing the file?

The answer came to me after I looked at this line:

setApplication(Application.create(config.APP));

Creating the application is now a global occurrence. That doesn’t necessarily stand out as odd but prior to fixing this file, only the acceptance test was failing and only acceptance tests were building the entire application. That was the overlap.

One piece of the puzzle was complete. Building the entire Ember app was causing problems. But why?

At this moment, I took a step back and combed over the initial problem. There were two different errors. Maybe they were caused by the same problem. Maybe that `lookup` error wasn’t as innocent as it seemed.

Now for some back story:

A long time ago we were having trouble with feature flags on Launch Darkly and identifying users with Segment. In order to feature flag correctly and target particular users differently, we had to know who a user was and this identity came from Segment. However, there was a race condition going on in the application. Sometimes, Segment would load first and we would properly identify a user before figuring out the feature flag. Sometimes, our application would try to grab a feature flag for an unknown user because Segment hadn’t loaded in time.

The tricky part to this race condition was that feature flags were evaluated all over the application in many different areas of the codebase. How could we ensure Segment was loaded before any one call to Launch Darkly was made?

The solution was to make sure both were initialized before the application booted and to ensure Segment was loaded prior to Launch Darkly in this one spot.

In Ember, code you need to run before the application boots is placed in an application initializer. This is where we placed our solution to the race condition.

Now the solution to the problem at hand.

The reason why this error was popping up:

Source:
TypeError: Cannot read property ‘lookup’ of undefined
at Object.initialize (http://localhost:4200/assets/my-app.js:10312:28)
at http://localhost:4200/assets/vendor.js:61627:21
at Vertices.each (http://localhost:4200/assets/vendor.js:80243:9)
at Vertices.walk (http://localhost:4200/assets/vendor.js:80157:12)
at DAG.each (http://localhost:4200/assets/vendor.js:80087:22)
at DAG.topsort (http://localhost:4200/assets/vendor.js:80095:12)
at Class._runInitializer (http://localhost:4200/assets/vendor.js:61653:13)
at Class.runInitializers (http://localhost:4200/assets/vendor.js:61625:12)
at Class._bootSync (http://localhost:4200/assets/vendor.js:59923:14)
at Class.boot (http://localhost:4200/assets/vendor.js:59890:14)

is because of the following code:

export function initialize(application) {
const container = application.__container__; // ← undefined
const lookup = container.lookup.bind(application.__container__); // ← error!
...
}

The lesson was learned the moment I opened up the initializer. We were using `application.__container__`!

In JavaScript, any method or variable that leads with an underscore is usually considered private. Two underscores means the same as one, but it’s a bit more emphatic — DON’T rely on it!

The application initializer was utilizing private Ember APIs to perform a lookup from the container. The initializer was performing a lookup to implicitly initialize the Segment service before retrieving Segment data and then initializing the Launch Darkly feature flag service.

What’s strange is the above code works in the development and production environments. This change seems to be a result of the deprecations / rearrangement of internal Ember APIs particularly related to the container. See this page for more information.

In order to access the container in an application initializer it must be done from an application instance (i.e., an instance initializer). This might have been an acceptable solution, but I wasn’t 100% sure if we could defer application readiness from an application instance initializer.

For more reading on the application instance initializers, see here.

Instead the code was moved to the application route’s beforeModel() hook. I also added an instance initializer for the Segment service which resulted in a faster application load time.

After moving the initializer code to the application route the application was now built successfully, all tests were passing again, and the acceptance tests worked like a charm. :)

Moral of the story / TL;DR:

Depend on things that change less than you do
- Sandi Metz

Looking to make an impact on a fast-growing technology team? Take a look at our open roles and apply today: ritual.com/careers

--

--