【问题标题】:Programmatic changes to Angular 2 component does not execute ngOnChanges in unit test对 Angular 2 组件的编程更改不会在单元测试中执行 ngOnChanges
【发布时间】:2017-04-30 09:21:31
【问题描述】:

我有一个相当简单的 Angular 2 组件。该组件根据我已排序的值数组呈现一些 div 兄弟元素。该组件实现OnChanges。排序功能发生在ngOnChanges() 的执行期间。最初,我的@Input() 属性引用了一个未排序的值数组。一旦更改检测开始,生成的 DOM 元素就会按预期排序。

我编写了一个 Karma 单元测试来验证组件中的排序逻辑是否已经发生,并且预期的 DOM 元素是否按排序顺序呈现。我以编程方式在单元测试中设置组件属性。调用fixture.detectChanges() 后,组件渲染其DOM。但是,我是 ngOnChanges() 函数,我看到它永远不会执行。对这种行为进行单元测试的正确方法是什么?

这是我的组件:

@Component({
  selector: 'field-value-map-item',
  templateUrl: 'field-value-map-item.component.html',
  styleUrls: [ 'field-value-map-item.component.less' ]
})
export class FieldValueMapItemComponent implements OnChanges {

  @Input() data: FieldMapping
  private mappings: { [id: number]: Array<ValueMapping> }

  ngOnChanges(changes: any): void {
    if (changes.data) { // && !changes.data.isFirstChange()) {
      this.handleMappingDataChange(changes.data.currentValue)
    }
  }

  private handleMappingDataChange(mapping: FieldMapping) {

    // update mappings data structure
    this.mappings = {};
    if (mapping.inputFields) {
      mapping.inputFields.forEach((field: Field) => {
        field.allowedValues = sortBy(field.allowedValues, [ 'valueText' ])
      })
    }

    if (mapping.valueMap) {
      // console.log(mapping.valueMap.mappings.map((item) => item.input))
      // order mappings by outputValue.valueText so that all target
      // values are ordered.
      let orderedMappings = sortBy(mapping.valueMap.mappings, [ 'inputValue.valueText' ])

      orderedMappings.forEach((mapping) => {
        if (!this.mappings[mapping.outputValue.id]) {
          this.mappings[mapping.outputValue.id] = []
        }
        this.mappings[mapping.outputValue.id].push(mapping)
      })
    }
  }

}

组件模板:

<div class="field-value-map-item">
  <div *ngIf="data && data.valueMap"
    class="field-value-map-item__container">
    <div class="field-value-map-item__source">
      <avs-header-panel
        *ngIf="data.valueMap['@type'] === 'static'"
        [title]="data.inputFields[0].name">
        <ol>
          <li class="field-value-mapitem__value"
            *ngFor="let value of data.inputFields[0].allowedValues">
            {{value.valueText}}
          </li>  
        </ol>

      </avs-header-panel>
    </div>
    <div class="field-value-map-item__target">
      <div *ngFor="let value of data.outputField.allowedValues">
        <avs-header-panel [title]="value.valueText">
          <div *ngIf="mappings && mappings[value.id]">
            <div class="field-value-mapitem__mapped-value"
              *ngFor="let mapping of mappings[value.id]">
              {{mapping.inputValue.valueText}}
            </div>
          </div>
        </avs-header-panel>
      </div>
    </div>
  </div>
</div>

这是我的单元测试:

describe('component: field-mapping/FieldValueMapItemComponent', () => {
  let component: FieldValueMapItemComponent
  let fixture:   ComponentFixture<FieldValueMapItemComponent>
  let de:        DebugElement
  let el:        HTMLElement

  beforeEach(() => {

    TestBed.configureTestingModule({
      declarations: [
        FieldValueMapItemComponent
      ],
      imports: [
        CommonModule
      ],
      providers: [ ],
      schemas: [
        CUSTOM_ELEMENTS_SCHEMA
      ]
    })

    fixture = TestBed.createComponent(FieldValueMapItemComponent)
    component = fixture.componentInstance

    de = fixture.debugElement.query(By.css('div'))
    el = de.nativeElement
  })

  afterEach(() => {
    fixture.destroy()
  })

  describe('render DOM elements', () => {

    beforeEach(() => {
      spyOn(component, 'ngOnChanges').and.callThrough()
      component.data = CONFIGURATION.fieldMappings[0]
      fixture.detectChanges()
    })

    it('should call ngOnChanges', () => {
      expect(component.ngOnChanges).toHaveBeenCalled()   // this fails!
    })

  })


  describe('sort output data values', () => {

    beforeEach(() => {
      component.data = CONFIGURATION.fieldMappings[1]
      fixture.detectChanges()
    })

    it('should sort source field allowed values by their valueText property', () => {
      let valueEls = el.querySelectorAll('.field-value-mapitem__value')
      let valueText = map(valueEls, 'innerText')
      expect(valueText.join(',')).toBe('Active,Foo,Terminated,Zip')   // this fails
    })
  })

})

【问题讨论】:

  • 或许可以选择将此组件作为单元测试中定义的虚拟父组件的子组件进行测试?

标签: javascript unit-testing angular ngonchanges


【解决方案1】:

另一种方式是直接调用ngOnChanges。

it('should render `Hello World!`', () => {
  greeter.name = 'World';

  //directly call ngOnChanges
  greeter.ngOnChanges({
    name: new SimpleChange(null, greeter.name)
  });
  fixture.detectChanges();
  expect(element.querySelector('h1').innerText).toBe('Hello World!');
});

参考:https://medium.com/@christophkrautz/testing-ngonchanges-in-angular-components-bbb3b4650ee8

【讨论】:

    【解决方案2】:

    解决这个问题的方法是在单元测试中引入一个简单的Component。这个Component 包含有问题的组件作为一个孩子。通过在此子组件上设置输入属性,您将触发 ngOnChanges() 函数,就像在应用程序中一样。

    更新了单元测试(注意顶部的简单@Component({...}) class ParentComponent)。在适用的测试用例中,我实例化了一个新的测试夹具和组件(类型为 ParentComponent),并设置了绑定到子组件输入属性的类属性,以触发ngOnChange()

    @Component({
      template: `
        <div id="parent">
          <field-value-map-item [data]="childData"></field-value-map-item>
        </div>
      `
    })
    class ParentComponent {
      childData = CONFIGURATION.fieldMappings[1]
    }
    
    describe('component: field-mapping/FieldValueMapItemComponent', () => {
      let component: FieldValueMapItemComponent
      let fixture:   ComponentFixture<FieldValueMapItemComponent>
      let de:        DebugElement
      let el:        HTMLElement
    
      beforeEach(() => {
    
        TestBed.configureTestingModule({
          declarations: [
            FieldValueMapItemComponent,
            ParentComponent
          ],
          imports: [
            CommonModule
          ],
          providers: [ ],
          schemas: [
            CUSTOM_ELEMENTS_SCHEMA
          ]
        })
    
        fixture = TestBed.createComponent(FieldValueMapItemComponent)
        component = fixture.componentInstance
    
        de = fixture.debugElement.query(By.css('div'))
        el = de.nativeElement
      })
    
      // ...
    
      describe('sort output data values', () => {
    
        let parentComponent: ParentComponent
        let parentFixture: ComponentFixture<ParentComponent>
        let parentDe: DebugElement
        let parentEl: HTMLElement
    
        beforeEach(() => {
          parentFixture = TestBed.createComponent(ParentComponent)
          parentComponent = parentFixture.componentInstance
    
          parentDe = parentFixture.debugElement.query(By.css('#parent'))
          parentEl = parentDe.nativeElement
    
          parentFixture.detectChanges()
        })
    
        it('should sort source field allowed values by their valueText property', () => {
          let valueEls = parentEl.querySelectorAll('.field-value-mapitem__value')
          let valueText = map(valueEls, 'innerText')
          expect(valueText.join(',')).toBe('Active,Foo,Terminated,Zip')
        })
    
        it('should sort target field mapped values by `inputValue.valueText`', () => {
          parentComponent.childData = CONFIGURATION.fieldMappings[2]
          parentFixture.detectChanges()
    
          let valueEls = parentEl.querySelectorAll('.field-value-mapitem__mapped-value')
          let valueText = map(valueEls, 'innerText')
          expect(valueText.join(',')).toBe('BRC,FreeBurrito,FreeTaco')
        })
      })
    
    })
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-12-15
      • 2017-10-10
      • 1970-01-01
      • 2016-04-09
      • 2018-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多