Thursday, February 11, 2016

Unit Testing Philosophy and TypeScript

In a previous post I talked about how you should organize your files based on the notion of coupling. The more connected two pieces of code are, the closer they should be. The reason is because when you modify a file you want the files you are most likely to also modify to be right there in your face. For example, a default pattern in Angular is to separate the templates from the controllers. Well, it turns out they are very tightly coupled and when you change one you are likely to have to change the other. This was so obvious to the Angular team itself that in Angular 2 they recommend a style where the template lives in the same file as the controller-like-object-that-is-not-really-called-a-controller.

Now, we switch gears for a second. How do we set up unit tests? Well, we build a completely separate project, code branch, or whatever. That is pretty much the standard. But based on the above notion that tightly coupled pieces of code should be together this is completely wrong. In most cases your code and the unit tests for it will be tightly coupled. In other cases (the best cases) the unit tests will be very de-coupled, but instead will offer excellent documentation of your code. This is one of those benefits that unit test proponents proclaim.

Why do we do this? Why do we separate out our unit tests so far from our code? The answer is pretty simple. We don't want to ship our unit tests. We don't want our tests in a production environment bloating up our code. For JavaScript, including the tests would be horrible because you would be sending code across the wire that would do nothing on the client. If only we had some way to include code in some builds and not in others... If only people had developed tools for this over 40-50 years ago. Of course, we can easily do this.

Here is what I think code should look like (note that I am using Jasmine - a popular JavaScript testing tool in this example):

var myFunction = (inputs) => {
    ...
}

describe("myFunction behavior", () => {
    it("does X", () => {
        expect(myFunction(myInputs)).toEqual(result);
    });
});

Now this makes plenty of assumptions. It assumes you are using Jasmine. It assumes a certain style of object creation. It assumes you want to test the function in isolation and not the object as a whole. The point is to illustrate the idea that code and tests for that code should be tightly linked.

Okay, now I do TypeScript. I don't necessarily love TypeScript because it limits certains valid and useful patterns. One thing I don't like about TypeScript is that it tries to make the class syntax like C#/Java where you can't put in lines that do anything between functio declarations: For example, the following code doesn't work:

class Foo {
    myFunction(inputs) {
        ...
    }
    describe("stuff", () => {
        it("does X", () => {...});
    });
}

TypeScript will barf on this even though it is basically valid and extremely useful. Okay, that isn't TypeScript really, but is actually the class keyword which is going into the language. One saving grace of TypeScript is the use of Decorators from the ES7 proposals. Decorators help restore some of the flexibility that the class keyword rips out. Okay, so here is the solution I am working on:

class Foo {
    myFunction(inputs) {
        ...
    }
    @Test("stuff")
    testFunction() {
        it("does X", () => {...});
    });
}

The @Test makes a handy thing to look for when stripping tests out for production code. It is also the following decorator:

var Test = (description) => {
    return (target, name, property) => {
        describe(description, property.value())
    }
});

Now this probably has some problem with the this keyword, but I think this is an extremely promising approach to unit testing in TypeScript. It also makes TypeScript unit tests look more like Java/C# unit tests and TypeScript lovers probably would dig that too.

No comments:

Post a Comment