Jorge Guerra Pires
Jorge Guerra Pires

Reputation: 632

Unit testing with Angular: how to test changes on parent to child

I am unit testing a component in Angular, using Jasmine. My test

it('contacts should be passed to child component', fakeAsync(() => {
    const newContact: Contact = {
        id: 1,
        name: 'Jason Pipemaker'
    };
    const contactsList: Array<Contact> = [newContact];
    contactsComponent.contacts = contactsList;//change on parent component
    tick()
    fixture.detectChanges();
    fixture.whenStable().then(() => {
        expect(childComponents()[0].contacts).toEqual(contactsComponent.contacts);//want to check if changed on child also
    })
}));

I want to test if a change in the parent will reflect on the child, like happens in a real scenario.

I have tested that the values are equal when everything starts, but I want to test the scenario when one changes the parent value, and that should automatically reflect on the child.

it('contacts should be the same', () => {
   expect(childComponents([0].contacts)
      .toEqual(contactsComponent.contacts);
});

Error:

Expected $.length = 0 to equal 1. Expected $[0] = undefined to equal Object({ id: 1, name: 'Jason Pipemaker' }).

My interpretation: it is not waiting for the update to test.

Maybe it is not even a unit test? Since I want to test the binding, maybe it is an integration test.

Suggestions so far

Suggestion 1: I would try with contactsComponent.contacts = JSON.parse(JSON.stringify(contactsList))

comments: Thanks, it does not work. See that I have already tested at the beginning: everything passes. The question would be. Why did it work at the beginning/launching but not after started? I guess this is a problem of somehow making sure everything finishes before testing: an async issue, not JSON.stringify. I have no idea how to make that in code terms! In simple terms: I am changing something on the parent, and want to make sure the child receives the change, like it happens on real scenarios. Thanks for your participation! 🤝👊💪 My HTML:

<app-contact-list [contacts]=contacts></app-contact-list>

PS. app-contact-list is the child:

export class ContactListComponent implements OnInit {
  .......
  @Input('contacts') contacts: Contact[];//the input for the component

Suggestion 2: I think you should test the component individually.

Comments: Regarding mocking the child, I am already mocking using ng-mock

beforeEach(async(() => {
   TestBed.configureTestingModule({
      declarations: [ContactsComponent, 
         MockComponent(ContactListComponent)],
   }).compileComponents();
}));

Drawback: it came to me as private lessons that some organizations do not allow installing those packages, thus, one should mock manually, as suggested.

Suggestion 3: You can avoid it by leveraging the async/await syntax.

Comments: tried, but did not work, same problem as before. it seems the code is executing before test. I face the same problem with Jest, learnt a trick, that that never happened before. I guess I am missing something like that, something that makes sure the test waits for the final modifications before testing.

Codes: some suggested making the codes available. Here it goes: https://github.com/JorgeGuerraPires/testing-angular-applications/tree/master/website/src/app/contacts

Maybe someone could make a StackBlitz as suggested!💪🤝👊

Final solution

In order to close this issue, I have decided to test in a simple case, as so the core idea under test would be the only concern. See the full simple application here before testing. And see the final testing file here.

I draw insights from both answers, and accepted the one that helped me the most, even the comments helped.

Thanks to everyone!

Upvotes: 4

Views: 7158

Answers (2)

IAfanasov
IAfanasov

Reputation: 4993

I like this approach of testing bindings and do the same in my code as well. The provided test is almost right. Just a small issue with asynchronicity. The code inside fixture.whenStable().then( would be executed after the test would be considered by karma as finished. You can avoid it by leveraging the async/await syntax.

it('contacts should be passed to child component', aynsc () => {
    const newContact: Contact = {
        id: 1,
        name: 'Jason Pipemaker'
    };
    const contactsList: Array<Contact> = [newContact];
    contactsComponent.contacts = contactsList;//set on parent component

    fixture.detectChanges();
    await fixture.whenStable();

    expect(childComponents()[0].contacts)
       .toEqual(contactsComponent.contacts);//want to check if changed on child also
}));

it('child component should receive data when data in parent is changed', aynsc () => {
    const firstContact: Contact = {
        id: 1,
        name: 'Jason Pipemaker'
    };
    const contactsList: Array<Contact> = [firstContact];
    contactsComponent.contacts = contactsList;//set on parent component
    fixture.detectChanges();
    await fixture.whenStable();
    const secondContact: Contact = {
        id: 1,
        name: 'Jason Pipemaker'
    };

    const contactsList2: Array<Contact> = [firstContact, secondContact];
    contactsComponent.contacts = contactsList2;//change on parent component
    fixture.detectChanges();
    await fixture.whenStable();

    expect(childComponents()[0].contacts)
       .toEqual(contactsComponent.contacts);//want to check if changed on child also
}));

Upvotes: 1

Robin Dijkhof
Robin Dijkhof

Reputation: 19288

I think you should test the component individually. In your ContactListComponent, test if it contains the right contacts. Something like the simple test below. Testing the child is a completly other test so mock the child component.

  it('have the correct contacts', () => {
    expect(component.contacts[0].id).toBe(1);
    expect(component.contacts[0].name).toBe('value');
  });

Simple mock example:

@Component({
  selector: 'child-component'
  template: ''
})
class TestChildComponent {
  @Input() contact;
}

Now, you want to test your ChildComponent. This can be done by stubbing a parent and test if the child reflects the correct data.

Example:

@Component({
  template: '<child-component [contact]="contact"></child-component>'
})
class TestHostComponent {
  contact = new Contact();
  @ViewChild(ChildComponent) component: ChildComponent;
}

describe('ChildComponent', () => {
  let component: ChildComponent;
  let host: TestHostComponent;
  let fixture: ComponentFixture<TestHostComponent>;


  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ChildComponent, TestHostComponent],
      ...
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestHostComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
    fixture.detectChanges();

    host = fixture.componentInstance;
    component = host.component;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should be able to present a contact', () => {
    host.contact = new Contact('otherValue');
    expect(SomeQuery.value).toBe('otherValue')
  }); 

});

EDIT:

Instead of mocking the child component, you could instead add the CUSTOM_ELEMENTS_SCHEMA.

TestBed.configureTestingModule({
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ],
  ...
});

This will prevent angular from raising errors when components could not be found. Use with caution because this could give the false impression everything is fine.

Upvotes: 1

Related Questions