1

I'm hesitating between algorithm and implementation as most of the questions here on how to prevent coupling between implementation and tests are about using spies/stubs or mocks.

The typical problem is that the tests are merely mirroring the SUT because tests are focusing validating behaviour instead of state.

The coupling I am talking about is where tests are validating the state of the SUT, but have to mirror the code in order to gather the necessary data to form the assertion.

Take for example this code that tests that the canvas has the correct dimensions:

it(
    'should set the width and height of the canvas', 
    fakeAsync(
        () => {
            fixture.detectChanges();
            getTestScheduler().flush();
            tick();

            let bordersWrapperStyle = getComputedStyle(fixture.nativeElement);
            let bordersWidth = bordersWrapperStyle
                .getPropertyValue('border-width')
                .split(' ');
            let yBordersTotalWidth = parseInt(bordersWidth[0]) * 2;
            let xBordersTotalWidth = parseInt(bordersWidth[1]) * 2;

            expect(page.canvasEl.width).toEqual(
                fixture.nativeElement.offsetWidth - xBordersTotalWidth
            );

            expect(page.canvasEl.height).toEqual(
                fixture.nativeElement.offsetHeight - yBordersTotalWidth
            );
        }
    )
);

Now the SUT:

setCanvasRect() {
    let rootEl = this.elRef.nativeElement;
    let bordersWrapperStyle = getComputedStyle(rootEl);
    let bordersWidth = bordersWrapperStyle.getPropertyValue('border-width').split(' ');
    let yBordersTotalWidth = parseInt(bordersWidth[0]) * 2;
    let xBordersTotalWidth = parseInt(bordersWidth[1]) * 2;

    this.canvasEl.width = rootEl.offsetWidth - xBordersTotalWidth;
    this.canvasEl.height = rootEl.offsetHeight - yBordersTotalWidth;
}

Maybe the answer is Sometimes it's unavoidable to have this kind of coupling between test and SUT.

  • Your tests need to stimulate and monitor the UUT. Monitoring the UUT means you need to check properties. Properties don't need to be fields. Maybe you could identify properties that could be extracted by a helper class; then you only need to write your tests against the helper, which forms a higher level interface to the UUT. – BobDalgleish May 24 '18 at 23:12
8

A way to avoid this duplication is to create a fixture with explicitly defined values. In this example you would setup the canvas with explicitly defined values for fixture.nativeElement.offsetWidth and xBordersTotalWidth. Then you would pre-compute the difference between the two. Your assertion would then check if the expected results came out. This would avoid the duplication you mention and thus also protect you against duplicating wrong calculations in the test code (and thus not truly testing the expected outcome). For example, if you defined your fixture such that fixture.nativeElement.offsetWidth was 310 and xBordersTotalWidth was 10, you would write your expectation as:

expect(page.canvasEl.width).toEqual(300);
  • I thought about this solution, but for some reason discarded it. I feel stupid now =). Thanks for the help! – maximedupre May 25 '18 at 0:13
3

I'd say your problem is that setCanvasRect is doing two things: it is both parsing a property and setting the properties on the canvas. Split the parsing out into a separate function / class / whatever, then you can test the two things separately:

  1. Test that parseBorders(element) correctly returns the borders for the given element (could perhaps break this down even more into fetching the style and parsing it).
  2. Test that setCanvasRect(w, h) sets the height and width correctly.
  3. Test that setCanvasRect(element) makes the correct calls to parseBorders(element) and setCanvasRect(w, h), mocking out the calls to parseBorders(element) and setCanvasRect(w, h).
  • I'm not sure this would work, as it would simply spread the same implementation logic into two tests instead of one. – maximedupre May 24 '18 at 22:52
1

The coupling I am talking about is where tests are validating the state of the SUT, but have to mirror the code in order to gather the necessary data to form the assertion.

TL; DR: The search term you want to explore is property based testing.

What you've run into is a common problem with "example based" tests; we have a known input, and therefore a specific output we are targeting, and if we apply enough "remove duplication" refactorings in the test code, we end up discovering the implementation we are trying to check.

Property based testing is a different idea: can we make progress with tests that check that answers have the right properties, without worrying too much about the specifics.

Scott Wlaschin wrote an introduction that I really like, featuring attempts to write tests that force a pathological adversary to provide a correct implementation of add(x,y).

The core idea being to dig around for things that should always be true: will width/height always by >=0 ? will they always be less than the extents of the native element? will the difference always be a multiple of two? If the height of the native element is larger than the width, does that mean the expected canvas height is always greater than the expected canvas width? and so on.

It's somewhat analogous to the treatment of post-conditions in design by contract; can you verify that the outcome of the operation satisfies a collection of invariants?

1

You could also extract the calculation part into a pure function, approximately like this:

setCanvasRect() {
    let rootEl = this.elRef.nativeElement;
    let bordersWrapperStyle = getComputedStyle(rootEl);

    let widthAndHeight = calculateWidthAndHeight(
        bordersWrapperStyle.getPropertyValue('border-width'),
        rootEl.offsetWidth,
        rootEl.offsetHeight);

    this.canvasEl.width = widthAndHeight[0];
    this.canvasEl.height = widthAndHeight[1];
}

calculateWidthAndHeight(bordersWidth, rootOffsetWidth, rootOffsetHeight) {
    let bordersWidthElems = bordersWidth.split(' ')
    let yBordersTotalWidth = parseInt(bordersWidthElems[0]) * 2;
    let xBordersTotalWidth = parseInt(bordersWidthElems[1]) * 2;
    return [rootOffsetWidth - xBordersTotalWidth, rootOffsetHeight - yBordersTotalWidth];
}

Then you can test with concrete examples

expect(calculateWidthAndHeight("10 5", 100, 200).toEqual([80, 190])

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.