Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing angular 2 Services does not run callback after injection

So I've created unit tests for my components but want some for my individual services in isolation. However when i try to inject them(the service method being tested is not async).

describe('SearchService', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                SearchService
            ]
        });

    });


    it("should build Url String", () => {
        inject([SearchService], (searchService: SearchService) => {
            spyOn(searchService, 'buildURL');
            console.log("should be logging like a boss");
            searchService.buildURL("test", "coursename", 2);
            expect(searchService.buildURL).toHaveBeenCalled();
            expect(searchService.buildURL("test", "coursename", 2)).toBe(['1']);
            expect(searchService.buildURL("test", "coursename", 2)).toBeFalsy();
        });
    });

});

Inject never actually runs callback! test recognizes it statement but passes without error.

the console.log statement inside never runs and tests designed to fail pass, so I assume inject is failing to run.

like image 305
deek Avatar asked Oct 03 '17 21:10

deek


2 Answers

You simply nested 1 extra clojure and that's why it won't work.

it("should build Url String", () => {
        inject([SearchService], (searchService: SearchService) => {
            spyOn(searchService, 'buildURL');
            console.log("should be logging like a boss");
            searchService.buildURL("test", "coursename", 2);
            expect(searchService.buildURL).toHaveBeenCalled();
            expect(searchService.buildURL("test", "coursename", 2)).toBe(['1']);
            expect(searchService.buildURL("test", "coursename", 2)).toBeFalsy();
        });
    });

Change it like below to make it work:

it("should build Url String", inject([SearchService], (searchService: SearchService) => {
            spyOn(searchService, 'buildURL');
            console.log("should be logging like a boss");
            searchService.buildURL("test", "coursename", 2);
            expect(searchService.buildURL).toHaveBeenCalled();
            expect(searchService.buildURL("test", "coursename", 2)).toBe(['1']);
            expect(searchService.buildURL("test", "coursename", 2)).toBeFalsy();
    })
);

The reason is, since you perform inject within another clojure it will perform it within another scope, the 2nd parameter of it should be a function with tests but since you passed in an empty clojure it will simply resolve to true.

Here's an example of what's going on:

() => { // calling this clojure it will return null/undefined
  () => { // calling this clojure it will return '1'
    return '1';
  }
}
like image 181
Ante Jablan Adamović Avatar answered Oct 30 '22 05:10

Ante Jablan Adamović


EDIT: 2 added complete example of how to Unit test Angular 2/4 service with HTTP call stub data replacing original example. Excellent example of unit testing a service IMO somewhat different than official and third party guide.

EDIT: re-read official guide and after @AnteJablanAdamović in the above comments pointed out it's supposed to be

it('should tell ROUTER to navigate when hero clicked',
  inject([Router], (router: Router) => { // ...
}));

https://angular.io/guide/testing#the-inject-function

I'm not sure if you can wrap it in an fakeasync(why not?) or async as a call back but this is the correct answer to my original question(how did nobody figure this out with a 50+ bounty and 10+ upvotes?!).

However strategy below is a cleaner/faster way to do this imo instead of pasting inject into every "it" statement by including it in BeforeEach;

It's a shame Karma or angular doesn't throw any error or warning flags.

Here's the original answer I provided but also works as an alternative way:


I used testBet.get to inject service in beforeEarch: Much better than what most guides suggest IMO.

Try this guide if your having issues testing services: covers simple or complex services with dependencies:

http://www.kirjai.com/testing-angular-services-with-dependencies/

 describe('SearchService', () => {
// IMPORTANT - declase variables we'll set in before each so every "it statement // can reach them

    let searchService: SearchService;
    let backend: MockBackend;
    let setupConnections;


        class MockActivatedRoute extends ActivatedRoute {
            constructor() {
                super();
                this.params = Observable.of({ 'searchterm': '*', 'sorttype': 'relevant', 'pagenumber': 1, 'facet': '' });
            }
        }
        const MockRouter = {
            navigate: (x) => ({
                then: () => ({})
            })
        };
        beforeEach(() => {
            TestBed.configureTestingModule({
                imports: [HttpModule],
                providers: [
// below required for HTTP substitution testing
                    MockBackend,
                    BaseRequestOptions,
                    {
                        provide: Http,
                        useFactory: (backend: MockBackend, options: BaseRequestOptions) => new Http(backend, options),
                        deps: [MockBackend, BaseRequestOptions]
                    },
                    AnalyticsService,
                    { provide: ActivatedRoute, useClass: MockActivatedRoute },
                    {
                        provide: Router,
                        useValue: MockRouter
                    },
                    SearchService
                ]
            });

// set our values in before each and use Testbed to inject services
        searchService = TestBed.get(SearchService);
        backend = TestBed.get(MockBackend);

you can set path with if statements like in guide link above for setupConnections but unless your doing something unusual with a call, you don't need to have path match so this is fine

 setupConnections = (backend: MockBackend, options: any) => {
            backend.connections.subscribe((connection: MockConnection) => {
                const responseOptions = new ResponseOptions(options);
                const response = new Response(responseOptions);
                connection.mockRespond(response);
            });
        };

        });

Note the async not fakeAsync!!!! Usually I use fakeAsync fine in component unit testing but I got some errors doing this when unit testing these services this way, YMMV

    it('should get suggestions for search drop down and match of mock results for test', async(() => {
        console.log('running get Suggestions');
// here we set out HTTP data response stub: put return data in body
        setupConnections(backend, {
            body: {
                suggestions:
                ["6-minute walk test",
            },
            status: 200
        });
// resolve HTTP call with subscribe and do test in call back.
        searchService.getSuggestions('test').subscribe((x) => {
            console.log(x);
            expect(x).toEqual(['6-minute walk test']);
        });

    });
like image 3
deek Avatar answered Oct 30 '22 04:10

deek