loading...

Gremlins In Angular Testing Pt.I

dobis32 profile image Scott VanderWeide ・3 min read

This is the first part of a series of writings where I describe an issue I was having while writing tests in Angular. These gremlins are typically small errors I made that took me way too long to find, considering how quickly I was able to correct them once they had been identified. I have a growing list of them that I’ve been keeping track of.

Initially, I created this list so that I could look back at my own oversight and have a good chuckle. I’m sharing them so that you too might have a good chuckle, but maybe these will actually prevent someone from making the same mistakes someday.

The gremlin I want to share in this article is a particularly nuanced one. It has to do with a mock class I was using to replace the AngularFirestore service, from the AngularFire library. Here is the mock class: (I’m sure someone out there will already see the problem.)

export class MockAngularFirestore {
    public add(data: any) {
        return Promise.resolve({ data: 'doc reference data', id: 'new_id' });
    }

    public delete() {
        return Promise.resolve();
    }

    public update(data: any) {
        return Promise.resolve();
    }

    public valueChanges(options?: any) {
        return of([
            { data: 'some_data1', id: 'id1' },
            { data: 'some_data2', id: 'id2' },
            { data: 'some_data3', id: 'id3' }
        ]);
    }

    public collection(path: string) {
        return {
            valueChanges: this.valueChanges,
            doc: this.doc,
            add: this.add
        };
    }

    public doc(id: string) {
        return {
            valueChanges: this.valueChanges,
            delete: this.delete,
            update: this.update
        };
    }
}

Here is the failing test: (There was also another failing test for an updatePost method as well, but it was failing for the same reason; that test is almost identical to the one shown below.)

it('should have a funciton for deleting a blog post specified by ID that calls the appropriate Firestore service funcitons', async () => {
        const id = 'doc_id';
        const collectionSpy = spyOn(TestBed.inject(AngularFirestore), 'collection').and.callThrough();
        const docSpy = spyOn(TestBed.inject(AngularFirestore), 'doc').and.callThrough();
        const deleteSpy = spyOn(TestBed.inject(AngularFirestore), <any>'delete').and.callThrough();

        await service.deletePost(<string>id);

        expect(collectionSpy).toHaveBeenCalled();
        expect(collectionSpy).toHaveBeenCalledWith(<any>'blog');
        expect(docSpy).toHaveBeenCalled();
        expect(docSpy).toHaveBeenCalledWith(<any>id);
        expect(deleteSpy).toHaveBeenCalled();
    });

See the problem? Here’s the only clue I had:

Logged error

I’m sure there are more experienced devs out there that already know what the problem is, but I did not at the time. I spent a good chunk of time being confused, whispering vulgarities at my machine. I felt like I was going crazy. (It’s going to happen one day, I’m sure.) As it turns out, the issue sprouted from my lack of familiarity with the nuances of using the keyword this.

Eventually, I decided to import my mock class into one of my components, created a class method within said component to call the function I was testing, and bound a click event to call said method. I was getting the same error in my browser’s dev console. Then I modified the doc method of my mock class to log this to the console:

    public doc(id: string) {
        console.log(this);
        return {
            valueChanges: this.valueChanges,
            delete: this.delete,
            update: this.update
        };
    }

This is where I realized the error of my ways; here is what printed to the console:

Log of "this"

Do you know what the problem is now? In case you don’t: the keyword this is referring to the collection method of my mock class and not the mock class itself. I felt pretty stupid, but programming has always made me feel stupid on a regular basis; it’s all business as usual. So how to rectify the situation? I made a simple modification to the collection method like so:

public collection(path: string) {
        return {
            valueChanges: this.valueChanges,
            doc: this.doc,
            add: this.add,
            delete: this.delete
        };
    }

Now the delete method is within the scope of the keyword this, and my test passed with no problems.

Looking back, I created this gremlin on accident, because I wanted to be able to spy on all of the functions of my mock class directly from the class itself. I’m sure there are better ways I could have written my mock class that would have caused me less trouble, but I digress.

Thank you for indulging in my recollection of a silly mistake!

Posted on May 26 by:

dobis32 profile

Scott VanderWeide

@dobis32

Tall human. Software Developer.

Discussion

markdown guide
 

I like your articles.
What about to put code like text, but not like screenshots? It will be easier to read.

 

I would love to do it that way, taking screenshots and pasting them together is a hassle. I just wasn't sure what the proper markup would be for embedding TypeScript code?

[EDIT] I've figured it out. I'll be updating the post with text instead of images.