简体   繁体   中英

Angular 2 unit testing data passed from parent component to child component

I have a question about the way I've seen (the very few) examples of testing of data passed down from a parent component into a child component. Currently, in the Angular2 docs , they're testing to see if data has been passed down from a parent component to a child by inspecting the dom values of the child component . The issue that I have with this approach is that it forces the parent's spec to know the html structure of the child component. The parent component's job is just to pass data into the child . An example...

I have a Story Component as follows:

'use strict';

import {Component, OnInit, Input} from '@angular/core';
import {StoryService} from '../../services/story.service';
import {StoryModel} from '../../models/story-model';
import {AlbumCover} from './album-cover/album-cover';
import {Author} from "./author/author";
import {StoryDuration} from "./story-duration/story-duration";

@Component({
    selector: 'story',
    templateUrl: 'build/components/story/story.html',
    providers: [StoryService],
    directives: [AlbumCover, Author, StoryDuration]
})

export class Story implements OnInit {
    @Input('id') id:number;
    public story:StoryModel;

    constructor(private storyService:StoryService) {}

    ngOnInit() {
        this.getStory();
    }

    private getStory() {
        this.storyService.getStory(this.id).subscribe(story => this.story = story);
    }
}

Notice how it has an AlbumCover Component dependency in the directives array in the @Component decorator.

Here is my Story template:

<div *ngIf="story">
    <album-cover [image]="story.albumCover" [title]="story.title"></album-cover>
    <div class="author-duration-container">
        <author [avatar]="story.author.avatar" [name]="story.author.name"></author>
        <story-duration [word-count]="story.wordCount"></story-duration>
    </div>
</div>

Notice the <album-cover [image]="story.albumCover" [title]="story.title"></album-cover> line where I'm binding the story.albumCover from the Story controller to the image property of the AlbumCover . This is all working perfectly. Now for the test:

import {provide} from '@angular/core';
import {beforeEach, beforeEachProviders, describe, expect, injectAsync, it, setBaseTestProviders, resetBaseTestProviders} from '@angular/core/testing';
import {HTTP_PROVIDERS} from '@angular/http';
import {BROWSER_APP_DYNAMIC_PROVIDERS} from "@angular/platform-browser-dynamic";
import {TEST_BROWSER_STATIC_PLATFORM_PROVIDERS, ADDITIONAL_TEST_BROWSER_PROVIDERS} from '@angular/platform-browser/testing';
import {ComponentFixture, TestComponentBuilder} from '@angular/compiler/testing';
import {Observable} from 'rxjs/Observable';

// TODO: this pattern of importing 'of' can probably go away once rxjs is fixed
// https://github.com/ReactiveX/rxjs/issues/1713
import 'rxjs/add/observable/of';

resetBaseTestProviders();
setBaseTestProviders(
    TEST_BROWSER_STATIC_PLATFORM_PROVIDERS,
    [BROWSER_APP_DYNAMIC_PROVIDERS, ADDITIONAL_TEST_BROWSER_PROVIDERS]
);

import {Story} from './story';
import {StoryModel} from '../../models/story-model';
import {StoryService} from '../../services/story.service';

var mockStory = {
    id: 1,
    title: 'Benefit',
    albumCover: 'images/placeholders/story-4.jpg',
    author: {
        id: 2,
        name: 'Brett Beach',
        avatar: 'images/placeholders/author-1.jpg'
    },
    wordCount: 4340,
    content: '<p>This is going to be a great book! I <strong>swear!</strong></p>'
};

class MockStoryService {
    public getStory(id):Observable<StoryModel> {
        return Observable.of(mockStory);
    }
}

describe('Story', () => {
    var storyFixture,
        story,
        storyEl;

    beforeEachProviders(() => [
        HTTP_PROVIDERS
    ]);

    beforeEach(injectAsync([TestComponentBuilder], (tcb:TestComponentBuilder) => {
        return tcb
            .overrideProviders(Story, [
                provide(StoryService, {
                    useClass: MockStoryService
                })
            ])
            .createAsync(Story)
            .then((componentFixture:ComponentFixture<Story>) => {
                storyFixture = componentFixture;
                story = componentFixture.componentInstance;
                storyEl = componentFixture.nativeElement;
                componentFixture.detectChanges();
            });
    }));

    describe(`ngOnInit`, () => {
        describe(`storyService.getStory`, () => {
            it(`should be called, and on success, set this.story`, () => {
                spyOn(story.storyService, 'getStory').and.callThrough();
                story.ngOnInit();
                expect(story.storyService.getStory).toHaveBeenCalled();
                expect(story.story.title).toBe('Benefit');
            });
        });
    });

    it('should not show the story component if story does not exist', () => {
        story.story = null;
        storyFixture.detectChanges();
        expect(storyEl.children.length).toBe(0);
    });

    it('should show the story component if story exists', () => {
        story.story = mockStory;
        storyFixture.detectChanges();
        expect(storyEl.children.length).not.toBe(0);
    });

    describe('story components', () => {
        beforeEach(() => {
            story.story = mockStory;
            storyFixture.detectChanges();
        });

        describe('album cover', () => {
            var element,
                img;

            beforeEach(() => {
                element = storyEl.querySelector('album-cover');
                img = element.querySelector('img');
            });

            it(`should be passed the story albumCover and title to the album cover component`, () => {
                expect(img.attributes.src.value).toBe(mockStory.albumCover);
                expect(img.attributes.alt.value).toBe(mockStory.title);
            });
        });

        describe('author', () => {
            var element,
                img,
                nameEl;

            beforeEach(() => {
                element = storyEl.querySelector('author');
                img = element.querySelector('img');
                nameEl = element.querySelector('.name');
            });

            it(`should be passed the author name and avatar`, () => {
                expect(img.attributes.src.value).toBe(story.story.author.avatar);
                expect(img.attributes.alt.value).toBe(story.story.author.name);
                expect(nameEl.innerText).toBe(story.story.author.name);
            });
        });

        describe('story duration', () => {
            var element;

            beforeEach(() => {
                element = storyEl.querySelector('.story-duration');
            });

            it(`should be passed the word count to generate the total read time`, () => {
                story.story.wordCount = 234234;
                storyFixture.detectChanges();
                expect(element.innerText).toBe(`852 min read`);
            });
        });
    });
});

Look at my describe('album cover'... . The way I'm passing this expectation is that I'm finding the <album-cover> element, then finding the <img> tag inside of it, then checking the <img> 's DOM attributes. To me, this expection should be inside of the album-cover.spec.ts - NOT the story.spec.ts .

My question is : is there a way to test if a parent component passed data into a child component without relying on reading dom values?

You can use overrideTemplate to pass a view just for the test.

   return tcb
        .overrideTemplate(AlbumCover, '<div>{{valueFromParent}}</div>')
        .overrideProviders(Story, [

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM