在角度组件的 ngOnInit() 函数中设置异步值的单元测试

Ral*_*alf 7 unit-testing typescript angular

我正在尝试对从组件的 ngOnInit() 函数中的异步服务设置的数组进行单元测试。

export class DashboardComponent implements OnInit {
  todos: Todo[] = [];

  constructor(public todoService: TodoService) {}

  ngOnInit(): void {
    this.todoService.getTodos()
      .then(todos => this.todos = todos.slice(1, 5));
  }
}
Run Code Online (Sandbox Code Playgroud)

当我尝试用这样的函数测试它时:

  it('should load four todos for dashboard', function(done) {
    expect(component.todos.length).toEqual(4);
  });
Run Code Online (Sandbox Code Playgroud)

我收到一个错误,即 0 不等于 4,因为承诺尚未解决。我可以让它工作的唯一方法是公开服务并运行“脏”代码:

  it('should load four todos for dashboard', function(done) {
    var todos = component.todoService.getTodos();
    todos.then(x => {
      expect(x.length).toEqual(6);
      expect(component.todos.length).toEqual(4)
    });
    done();
  });
Run Code Online (Sandbox Code Playgroud)

但肯定有更好、更干净的方法来做到这一点,所以欢迎提出任何改进建议!

编辑 1:在 dbandstra 的提示之后,指向正确的方向,我想出了这个代码:

describe('DashboardComponent', () => {
  beforeEach( async(() => {
    TestBed.configureTestingModule({
      declarations: [
        DashboardComponent, EmptyComponent,
        RouterLinkStubDirective, RouterOutletStubComponent
      ],
      providers: [{provide: TodoService, useClass: FakeTodoService}]
    })
      .overrideComponent(TodoSearchComponent, EmptyComponent)
      .compileComponents()
      .then(() => {
        fixture = TestBed.createComponent(DashboardComponent);
        comp    = fixture.componentInstance;
      });
  }));

  beforeEach(() => {
    // TodoService actually injected into the component
    let todoService = fixture.debugElement.injector.get(TodoService);

    let spy = spyOn(todoService, 'getTodos')
      .and.returnValue(Promise.resolve(todos));

    // trigger initial data binding
    fixture.detectChanges();
  });

  it('should load four todos for dashboard', () => {
    fixture.whenStable().then(() => { // wait for async getTodos
      fixture.detectChanges();        // update view with todos
      expect(comp.todos.length).toEqual(4);
    });
  })
});
Run Code Online (Sandbox Code Playgroud)

这就像一个魅力,谢谢!

编辑 2:我也让它与/sf/answers/2761179431/ 中建议的解决方案一起工作,方法是让假服务像这样同步:

class TodoServiceMock{
  private data: Todo[];

  getTodos() {
    let todos = [ ...insert fake data here... ];
    this.data = todos;
    return this;
  }

  then(callback) {
    callback(this.data);
  }
}
Run Code Online (Sandbox Code Playgroud)

小智 0

这是为您提供的解决方案:

dashboard.component.ts:

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements OnInit {
  /* We don't need to set an empty array here */
  todos: Todo[];

  constructor(private todoService: TodoService) {}

  ngOnInit(): void {
    this.todoService.getTodos()
      .then(todos => this.todos = todos.slice(1, 5));
  }
}

Run Code Online (Sandbox Code Playgroud)

dashboard.component.spec.ts

describe('DashboardComponent', () => {
  let component: DashboardComponent;
  let fixture: ComponentFixture<DashboardComponent>;

  let todoService: TodoService;

  const todos = [
    { id: 'id1' },
    { id: 'id2' },
    { id: 'id3' },
    { id: 'id4' },
    { id: 'id5' },
    { id: 'id6' },
  ] as Todo[];

  beforeEach(() => {
    TestBed
      .configureTestingModule({
        declarations: [DashboardComponent],
        providers: [
          /* Stub the TodoService as empty object */
          { provide: TodoService, useValue: {} },
        ],
      })
      .compileComponents();
  });

  /**
   * Creating the component instance in another beforeEach()
   * to decompose module configuration and setting values.
   */
  beforeEach(() => {
    fixture = TestBed.createComponent(DashboardComponent);
    component = fixture.componentInstance;

    /* Injecting the TodoService and mocking used methods. */
    todoService = TestBed.inject(TodoService);
    todoService.getTodos = jasmine.createSpy('getTodos').and.resolveTo(todos);
  });

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

  describe('#ngOnInit', () => {
    /* Using fakeAsync for having the ability to wait for async operation. */
    it('should load four todos for dashboard', fakeAsync(() => {
      /* Running ngOnInit by calling the first detection of changes. */
      fixture.detectChanges();

      /**
       * Waiting for .then() method response,
       * because we cannot check the result immediately after running ngOnInit,
       * it's promises, not primitive code.
       */
      tick();

      /**
       * We use toBe() in comparing primitive types.
       * toEqual() is used for comparing not primitive types - objects, arrays, etc.
       */
      expect(component.todos.length).toBe(4);
    }));
  });
});
Run Code Online (Sandbox Code Playgroud)

在 Angular 中学习 Jasmine 的推荐资源: https://codecraft.tv/courses/angular/unit-testing/jasmine-and-karma/