Categories
Software

Unexpected Lessons from 100% Test Coverage

The conventional wisdom of the software engineering community is that striving to 100% test coverage is a fool’s errand. It won’t necessarily help you catch all bugs, and it might lead you down questionable paths when writing your code.

My recent attempts at 100% test coverage showed me the answer is much more subtle. At times I was tempted to make questionable code changes just for the sake of coverage. In some of those times, I did succumb. Yet, I found that often, there is an enlightened way to both cover a branch and make the code better for it. Blind 100% coverage can cause us to make unacceptable compromises. If we constrain ourselves with only making the codebase better, however, then thinking about 100% coverage can change the way we think about a codebase. The story of my 100% test coverage attempt is a story of both the good and the bad.


Last year I came across a thread from NPM creator Isaac Z. Schlueter advocating for 100% test coverage:

Schlueter alluded to a mentality shift that a developer achieves that piqued my interest:

Road to 100

Coveralls.io screen grab for schema-dts showing how it got to 100% Test Coverage

I decided that schema-dts would be the perfect candidate for the 100% test coverage experiment. Given N-Triples as an input, schema-dts generates TypeScript types describing valid JSON-LD literals for that ontology. I’ve been more and more interested recently in getting it to stability, and understanding where there’s headroom in the codebase.

The Setup

To start, I didn’t have a way to compute test coverage of my project in its current state. I ended up switching my test runner from Jasmine to Mocha for its lcov support. This being a TypeScript project, though, I had to enable source maps, and use ts-node to get coverage numbers of my actual .ts source (PR #56). I used Istanbul’s nyc to get coverage runs working locally. Coveralls integrates with nyc nicely to host online tracking of code coverage time. Coveralls also integrates seamlessly with Travis CI and gates all PRs by their ฮ”Coverage (PR #57).

My first real run after setting things up had a %78.72 test coverage. That’s not too bad, I thought. My existing tests belonged to broadly two general categories:

These baseline tests definitely covered a lot of lines of code that they didn’t really exercise, which is part of why that number was high. That itself can be an argument that 100% test coverage is a meaningless number. Schlueter’s promise of 100% test coverage, however, is that the act of getting to that long tail can have transformative effects of how I think about my own code. I wanted to try my luck at that first hand. If we wanted to be more confident about our covered lines are truly being tested, mutation testing might do better wonders than test coverage.

Happy Times: The Low Hanging Fruit

A Schema.org-like ontology can declare a certain class, property, or enum value as deprecated by marking it with a supersededBy predicate. schema-dts handles this in one of two ways: either marking it with @deprecated JSDoc comments in the code, or stripping those declarations entirely.

Looking at my coverage report, swaths of untested code become apparent. For example, I never attempted to generate a @deprecated class. Ok, let’s fix that. And I catch a real bug that my few unit tests hadn’t caught. I increased coverage by 9.8%, added some baseline tests of deprecation, and added some N-Triple parsing unit tests that I had never gotten around to.

Categories
Software

Use trackBy in Angular ngFor Loops and MatTables

A missing trackBy in an ngFor block or a data table can often result in hard-to-track and seemingly glitchy behaviors in your web app. Today, I’ll discuss the signs that you need to use trackBy. But firstโ€”some context:

More often than not, you’ll want to render some repeated element in Angular. You’ll see code that looks like this:

<ng-container *ngFor="let taskItem of getTasks(category)">

In cases where the ngFor is looping over the results of a function that are created anew each time (e.g. an array being constructed using .map and .filter), you’ll run into some issues.

Every time the template is re-rendered, a new array is created with new elements. While newly-created array elements might be equivalent to the previous ones, Angular uses strict equality on each element to determine how to handle it.

In cases where the elements are an object type, strict equality will show that each element of the array is new. This means that a re-render would have a few side-effects:

  • Angular determines all the old elements are no longer a part of the block, and
    • destroys their components recursively,
    • unsubscribes from all Observables accessed through an | async pipe from within the ngFor body.
  • Angular finds newly-added elements, and
    • creates their components from scratch,
    • subscribing to new Observables (i.e. by making a new HTTP request) to each Observable it accesses via an | async pipe.

This also leads to a bunch of state being lost:

  • selection state inside the ngFor is lost on re-render,
  • state like a link being in focus, or a text-box having filled-in values, would go away.
  • if you have side-effects in your Observable pipes, you’ll see those happen again.

The Solution

trackBy gives you the ability to define custom equality operators for the values you’re looping over. This allows Angular to better track insertions, deletions, and reordering of elements and components within an ngFor block.

Categories
Random Software

The Joys and Happy Accidents of Branching Out

How helping on a film set led to me down a serendipitous path, publishing a new open source library, and getting an IMDB mention.

About a year ago, a friend asked meโ€”along with some othersโ€”to help as extra hands on set filming the second season of an absurdist comedy mini-series she was working on called Look it Up.

Helping film anything wasn’t something I thought I would ever do, so I was excited to try it out. We weren’t necessarily trusted with anything difficult; carry equipment around, slate shots, clear the set, and move things around in general. It was interesting to see how much thought and effort goes into composing a shot, or storyboarding a scene.

Filming took place on a weekend. Five episodes, each under 2 minutes, took up about 2 days to film (including make-up, breaks, and lots of setup in between).

One of the cool things I saw in the filming process was how sound was treated. In the first season, the show’s creators enlisted help of another friend, Elizabeth McClanahan, to be their re-recording mixer. Her help salvaging a lot of the audio, mixing some sound effects for them, and also working with them to re-record some of the dialog was eye opening, and they entered their second season hoping to avoid some of the same mistakes (and try to learn how do to the sound themselves). So when I was there for the second season, we heard how a boom operator moving the mic too fast is enough to ruin the shot with wooshing air, and made a point to be silent, slow, and deliberate. I don’t think I had been as still for as long in quite a while.

Beyond being interesting, though, going out of my comfort zone for a weekend helped in a few fun ways:

  1. Being physically present, I was tasked with playing an extra for an opening credit. For some reason, that came with an IMDB credit. Kind of cute to cross off an item that wasn’t even on your bucket list to begin with.
  2. Caring about a friend’s artistic endeavor (wanting it to do well, get traction, etc.) caused me to think about practical tech solutions to problems I hadn’t occupied myself with.
Categories
Software

Learning by Implementing: Observables

Sometimes, the best way to learn a new concept is to try to implement it. With my journey with reactive programming, my attempts at implementing Observables were key to to my ability to intuit how to best use them. In this post, we’ll be trying various strategies of implementing an Observable and see if we can make get to working solution.

I’ll be using TypeScript and working to implement something similar to RxJS in these examples, but the intuition should be broadly applicable.

First thing’s first, though: what are we trying to implement? My favorite way or motivating Observables is by analogy. If you have some type, T, you might represent it in asynchronous programming as Future<T> or Promise<T>. Just as futures and promises are the asynchronous analog of a plain type, an Observable<T> is the asynchronous construct representing as collection of T.

The basic API for Observable is a subscribe method that takes as bunch of callbacks, each triggering on a certain event:

interface ObservableLike<T> {
  subscribe(
      onNext?: (item: T) => void,
      onError?: (error: unknown) => void,
      onDone?: () => void): Subscription;
}

interface Subscription {
  unsubscribe(): void;
}

With that, let’s get to work!

First Attempt: Mutable Observables

One way of implementing an Observable is to make sure it keeps tracks of it’s subscribers (in an array) and have the object send events to listeners as they happen.

For the purpose of this and other implementations, we’ll define an internal representation of a Subscription as follows:

Categories
Software

Schema.org Classes in TypeScript: Properties and Special Cases

In our quest to model Schema.org classes in TypeScript, we’ve so far managed to model the type hierarchy, scalar DataType values, and enums. The big piece that remains, however, is representing what’s actually inside of the class: it’s properties.

After all, what it means for a JSON-LD literal to have "@type" equal to "Person" is that certain properties โ€” e.g. "birthPlace" or "birthDate", among others โ€” can be expected to be present on the literal. More than their potential presence, Schema.org defines a meaning for these properties, and the range of types their values could hold.

The easy case: Simple Properties

You can download the entire vocabulary specification of Schema.org, most of which describes properties on these classes. For each property, Schema.org will tell us it’s domain (what classes have this property) and range (what types can its values be). For example, the name property specification shows that it is available on the class Thing, and has type Text. One might represent this knowledge as follows:

interface ThingBase {
  "name": Text;
}

Linked Data, it turns out, is a bit richer than that, allowing us to express situations where a property has multiple values. In JSON-LD, this is represented by an array as the value of the property. Therefore:

interface ThingBase {
  "name": Text | Text[];
}

Multiple Property Types

Often times, however, the range of a particular property is any one of a number of types. For example, the property image on Thing can be an ImageObject or URL. Note, also, that nothing in JSON-LD necessitates that all potential values of image have the same type.

In other words, if we want to represent image on ThingBase, we have:

interface ThingBase {
  "name": Text | Text[];
  "image": ImageObject | URL | (ImageObject | URL)[];
}

Properties are Optional

In JSON-LD, all properties are optional. In practice Schema.org cares about "@type" being defined for all classes, but does not otherwise define any other properties as being required. This is sometimes complicated as specific search engines require some set of properties on a class.

interface ThingBase {
  "name"?: Text | Text[];
  "image"?: ImageObject | URL | (ImageObject | URL)[];
}

Properties Can Supersede Others in the Vocabulary

As Schema.org matures, it’s vocabulary changes. Not all of these changes will be additive (adding a new type, or a new type on an existing property). Some will involve adding a new type or property intended to replace another.