0
votes

I'm trying to test my Angular component with Jasmine. The component is a simple form that submits some search criteria to a service which then goes off and does the Http stuff and returns an array of entities.

I am using Jasmine to 'spyOn' the service method and then return a mock entity. This mock entity should then be saved in a variable in the component.

The problem I am facing is that when I come to assert that the entity has been successfully returned, I am getting undefined in the entities variable which makes me think I haven't set up my spy correctly or something similar.

Any help will be greatly appreciated!

Service:

@Injectable()
export class DynamicsSearchService {
    private apiUrl = '/api/DynamicsSearch/Search';
    private headers = new Headers({ 'Content-Type': 'application/json' });

    constructor(private http: Http) { }

    search(search: DynamicsSearch): Promise<any[]> {
        search.fields = this.getDefaultFields(search.entity);
        return this.http
            .post(this.apiUrl, JSON.stringify(search), { headers: this.headers })
            .toPromise()
            .then((response) => { return this.extractResults(search.entity, response.json()); })
            .catch(this.handleError);
    }

    ...
}

Component:

@Component({
    selector: 'dynamics-search-component',
    templateUrl: 'dynamics-search.component.html'
})
export class DynamicsSearchComponent {
    ...

    entities: any[];

    constructor(private searchService: DynamicsSearchService) { }

    submitSearch() {
        this.searching = this.searched = true;
        this.searchService.search(this.model)
            .then(results => {
                this.entities = results;
                this.searching = false;
                this.searchSuccessful = results !== null && results.length > 0;
            });
    }

    ...
}

Test:

describe('DynamicsSearchComponent', () => {

    let fixture: ComponentFixture<DynamicsSearchComponent>;
    let component: DynamicsSearchComponent;

    let configuration = new Configuration();

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [
                FormsModule,
                SharedModule
            ],
            providers: [
                BaseRequestOptions,
                MockBackend,
                DynamicsSearchService,
                Configuration,
                {
                    provide: Http,
                    useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
                        return new Http(backend, defaultOptions);
                    },
                    deps: [
                        MockBackend,
                        BaseRequestOptions
                    ]
                }
            ],
            declarations: [
                DynamicsSearchComponent
            ]
        }).compileComponents();
    });

    beforeEach(() => {
        fixture = TestBed.createComponent(DynamicsSearchComponent);
        component = fixture.componentInstance;
    });

    it('on submit should get a single contact',
        inject([DynamicsSearchService], (service: DynamicsSearchService) => {
            var expected = [
                {
                    contactid: 'A7806F57-002C-403F-9D3B-89778144D3E1'
                }
            ];

            const spy = spyOn(service, 'search')
                .and.returnValue(Promise.resolve(expected));            

            component.model = new DynamicsSearch('contacts', 'A7806F57-002C-403F-9D3B-89778144D3E1', null, 'contactid');
            component.submitSearch();

            fixture.detectChanges();

            expect(spy.calls.count()).toBe(1, `expected service search method to be called once but was called ${spy.calls.count()} times`);
            expect(component.entities).toBeDefined('no entities returned');
            expect(component.entities.length).toBe(1, `expected 1 entity to be returned but only ${component.entities.length} were returned`);
        }
    ));
});

It fails on the second expect because component.entities is undefined.

1
You should split this into two separate tests - it's a really bad sign that you're pulling in Http to test a component. Test the service with the MockBackend, then test the component with a fake service. Also you probably will need to fixture.detectChanges() to ensure everything is updated after the service data comes back. - jonrsharpe
Thanks Jon. I'll bear this is mind going forward and try to refactor out the service bits once/if I can figure out this problem. Also added a fixture.detectChanges() after submitting the search which doesn't seem to have helped. Updated above. - Andy Furniss
My point is that will help you now to isolate the issue and create a minimal reproducible example. I wrote up some of our service and component testing on my blog for some colleagues recently, that may be of use: blog.jonrshar.pe/2017/Apr/16/async-angular-tests.html - jonrsharpe
Yeah, you're right. I more wanted to figure out the reason why it was failure so that I can learn but maybe that's the wrong way around. Julia's answer seems to have fixed the issue as it stands so now I will move forward by separating out the service bits into it's own test next. - Andy Furniss
@jonrsharpe I have used your blog post as a reference whilst refactoring my component test to create a service spy and it proved very helpful after I was frustrated whilst trying to figure out how to mock the service correctly. Thank you again. - Andy Furniss

1 Answers

1
votes

You are working with Promise that is async code. Put expect into fixture.whenStable func and add async function into 'it' unit test.

fixture.whenStable().then(() => {
    expect(component.entities).toBeDefined('no entities returned');
});