Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create custom jasmine matcher using Typescript

I'm using jasmine on an angular2 project and having some trouble writing a custom matcher for a test. I want to be able to compare two relatively complex objects. I found this article which claims to solve the issue but it simply results in a typescript error stating that it doesn't recognize the new method on jasmine's Matchers object. The relevant code is this:

declare module jasmine {
    interface Matchers {
        toBeNumeric(): void;
    }
}

Another article gives a similar, but slightly different solution that gives the same error.

declare namespace jasmine {
    interface Matchers {
        toHaveText(expected: string): boolean;
    }
}

I tried this

let m: jasmine.Matchers = expect(someSpy.someMethod).toHaveBeenCalled();

and got this error:

Type 'jasmine.Matchers' is not assignable to type 'jasmine.Matchers'. Two different types with this name exist, but they are unrelated.

That seems to indicate that the declare namespace jasmine statement is creating a new jasmine namespace rather than extending the existing one.

So how can I create my own matcher that typescript will be happy with?

like image 235
d512 Avatar asked Mar 22 '17 15:03

d512


3 Answers

Daf's answer mostly worked for me I just noticed an issue with his sample code and the way he named his files. I also happened upon another unrelated issue. Hence a new answer.

  • For some reason my app does not like it when the interface file has the same name as the matcher file. e.g foo.ts and foo.d.ts. For my app it needed to be foo.ts and foo-interface.d.ts or something like it.
  • Also don't import interfaces from foo.ts into foo-interface.d.ts it also does not seem to like this.

Matcher - custom-matchers.ts

import MatchersUtil = jasmine.MatchersUtil;
import CustomMatcherFactories = jasmine.CustomMatcherFactories;
import CustomEqualityTester = jasmine.CustomEqualityTester;
import CustomMatcher = jasmine.CustomMatcher;
import CustomMatcherResult = jasmine.CustomMatcherResult;

export const SomeCustomMatchers: CustomMatcherFactories = {
    toReallyEqual: function (util: MatchersUtil, customEqualityTester: CustomEqualityTester[]): CustomMatcher {
        return {
            compare: function (actual: any, expected: any, anotherCustomArg: any): CustomMatcherResult {

                // Your checks here.
                const passes = actual === expected;

                // Result and message generation.
                return {
                    pass: passes,
                    message: passes ? `Actual equals expected`
                                    : `Actual does not equal expected`,
                }
            }
        }
    }
};

NOTE that compare function can have as many custom-parameters as we want (or even Variadic), and that ONLY first-argument is required/reserved (to know actual-value); but if the function name begins with "toHave" (instead of toReallyEqual), then the second argument is reserved for "key: string" (to know object's field name, I mean, Jasmine2 will loop for us).

Also, we could relay on Jasmine for message-generation, like:

message: util.buildFailureMessage('toReallyEqual', passes, actual, expected, anotherCustomArg),

Interface file - matcher-types.d.ts - cannot be the same name as your matcher file

declare namespace jasmine {
    interface Matchers<T> {
        toReallyEqual(expected: any, anotherCustomArg: any, expectationFailOutput?: any): boolean;
    }
}

Custom matcher test

describe('Hello', () => {

    beforeEach(() => {
        jasmine.addMatchers(SomeCustomMatchers)
    });

    it('should allow custom matchers', () => {
        expect('foo').toReallyEqual('foo');
        expect('bar').not.toReallyEqual('test');
    })
});
like image 187
Cameron Avatar answered Nov 15 '22 00:11

Cameron


Basically, your second example ("declare namespace") is the way to go, with your logic for the matchers somewhere else, of course.

You're welcome to take a look at https://github.com/fluffynuts/polymer-ts-scratch/tree/5eb799f7c8d144dd8239ab2d2bcc72821327cb24/src/specs/test-utils/jasmine-matchers where I have written some Jasmine matchers and typings to go along with them -- though technically I wrote the actual matchers in Javascript and just named the logic files .ts to placate my build process.

You will need to install @types/jasmine -- and keep it current.

Just bear in mind that different versions of @types/jasmine may break things; specifically, the commit linked above was when Jasmine types introduced the Matchers type having a type parameter (ie, Matchers<T>) which broke all my .d.ts files.

like image 36
daf Avatar answered Nov 14 '22 23:11

daf


If you're using ES modules then the namespace declaration needs to be wrapped in a declare global block.

Here is the updated example with both custom matchers merged into the same definition:

    declare global {
        namespace jasmine {
            interface Matchers {
                toBeNumeric(): void;
                toHaveText(expected: string): boolean;
            }
        }
    }

It's also possible to have the declarations split apart (or even spread across multiple files):

    // File 1 declares this matcher
    declare global {
        namespace jasmine {
            interface Matchers {
                toBeNumeric(): void;
            }
        }
    }

    // File 2 declares this matcher
    declare global {
        namespace jasmine {
            interface Matchers {
                toHaveText(expected: string): boolean;
            }
        }
    }

    // File 3: use the custom matchers
    it(function(){
        expect(3).toBeNumeric();
        expect(result).toHaveText('custom matcher');
    });

NOTE: In these snippets the namespace and module keywords are equivalent but module is deprecated and namespace is preferred (to avoid confusion with ES modules or CommonJS modules).

like image 2
Sly_cardinal Avatar answered Nov 15 '22 00:11

Sly_cardinal