6

零、前言

学习Angular的时候,总感觉特别的复杂、高级,以至于产生畏惧心理,这种心理尤其体现在单元测试上。

今天被醍醐灌顶之后,感觉单元测试的报错信息也不是那么难看懂了。

(文章的后半段是记录我自己的一次单元测试的过程,为了不耽误读者时间,我把结论写在第一小节。)

一、分析报错信息

启动单元测试后,映入眼帘的是一堆信息:

图片.png

先来看顶部的信息:

图片.png

最上面的一排点···············是整个项目的测试总数,下面的Ran 1 of 58 specs是一共58个测试,本次测试启动了一个。

下面的1 spec, 1 failure是本次启动的一个测试中,有一个出错了,然后列出了所有报错的测试项,以及错误信息。

然后看错误信息:

图片.png

顶上的 TypeError: Cannot set property 'workId' of undefined 是主要的错误类型。

本文举例的错误是类型错误,不能对一个 undefined 对象调用 workId 属性。

下面的那一大片代码,是方法的堆栈
举个例子:如果 A( )调用了 B( ), B( ) 调用了C( ),那么堆栈情况就是这样:

图片.png

我们知道,栈是先进后出,队列是先进先出,这里使用的就是栈。

  • 当A( )方法被调用时,内存加载A( ),此时A就在栈上
  • 执行的时候突然发现,A( )需要调用B( )方法,此时应该加载B( )
  • 但A( )还没有执行完毕,不能释放,所以就用栈的方式,把A( )压在下面并且冻结
  • B( )的执行过程中又需要C( ),所以B冻结,C入栈。
  • 等到C( )执行完之后,把结果返回给B( ),此时C( )被释放,出栈
  • B( )被解冻,拿到C( )的返回值,继续执行后面的语句
  • 后面的过程同理......

所以,这张图的意思,就是堆栈,最上面的方法,是当前的活跃方法,也就是出错的方法:

图片.png

可以清晰的看到at WorkStubService.getById (http://localhost:9877/_karma_webpack_/src/app/service/service-tesing/work-stub.service.ts:76:32)

翻译一下就是,错误发生在WorkStubService类的getById方法,链接的最后显示了,是work-stub.service.ts文件的第76行的第32个字符,出现了问题。

然后定位到这行代码,就可以清楚的找到问题了。

分析报错信息的方法介绍,到此结束。

二、记一次排错实录

计划测试EditComponent组件的C层初始化。

C层:

export class EditComponent implements OnInit {

  work = new Work();
  params = {
    workId: 0
  };
  constructor(private workService: WorkService) {
  }

  ngOnInit() {
    this.params.workId = 0;
    this.load();
  }


  public load() {
    this.workService.getById({id: this.params.workId})
      .subscribe((data) => {
        this.work = data;
        console.log(this.work);
      }, () => {
        console.log('error');
      });
  }
  
}

ngOninit调用load方法,load调用service来获取数据,

所以测试的思路就是让组件初始化,然后断言C层向M层的传值,以及断言M层的返回值即可。

describe('Page -> Teacher -> EditComponent', () => {
  let component: EditComponent;
  let fixture: ComponentFixture<EditComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ EditComponent ],
      imports: [
        HttpClientTestingModule,
      ],
      providers: [
        {provide: WorkService, useClass: WorkStubService}
      ]
    })
      .compileComponents();
  }));

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

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


  it('组件初始化发起请求测试', () => {
    /* 获取请求参数 */
    const workService: WorkStubService = TestBed.get(WorkService);
    const queryParam = workService.pageParamsCache;

    /* 断言传入的参数值与组件中的参数值相同 */
    expect(queryParam.workId).toEqual(component.params.workId);
  });


  it('组件初始化V层渲染', () => {
    /* 断言总行数及第一行的内容绑定符合预期 */

    expect(component.work.id).toBe(1);
    expect(component.work.content).toBe('<p>content</p>');
    expect(component.work.item).toBe(new Item({name: 'Item'}));
    expect(component.work.score).toBe(100);
    expect(component.work.student).toBe(new Student({name: 'Student'}));
    expect(component.work.reviewed).toBe(true);

  });
});

一共三个小的测试项。为了模拟返回值,还需要一个假的M层。

export class WorkStubService {

  constructor() {
  }

  /* 传入参数缓存 */
  pageParamsCache: { page: number, size: number ,workId: number};

  /**
   * getById模拟方法
   * @param params 查询参数
   */
  getById(params: { id: number }): Observable<Work> {
    this.pageParamsCache.workId = params.id;
    const mockResult = new Work(
      {
            id: 1, content: '<p>content</p>', item: new Item({name: 'Item'}),
            score: 100, student: new Student({name: 'Student'}), reviewed: true});
    return of(mockResult);
  }
}

然后一运行测试,我懵了,所有模拟的返回值,全都是undefined:
图片.png

带着疑惑的心态,开始看自己的代码,感觉哪里都没问题...
更何况这些代码是从老项目里面粘过来的,“不可能”出错。

为了找出不同,我把代码和老项目一行一行的对照,的确没有问题。

然后我发现了模拟M层的getById方法是灰色,还固执的认为可能是没有被调用:
图片.png

我又打了一堆断点,事实证明,即使是灰色,也确实被执行了。
图片.png

直到这时,我还没有仔细的看报错信息。

终于,无法自己解决了,只能问老师,然后才发现,关键在于报了一个TypeError: Cannot set property 'workId' of undefined
图片.png

一看,undefined,于是又傻傻的去C层找定义变量的代码,
图片.png
都有初始值,怎么会是undefined呢??

后来老师又提示了,看报错信息。才知道那一大堆信息,实际上是堆栈,如果要找到具体的出错位置,只需要看最上面的一行,就可以了。
at WorkStubService.getById (http://localhost:9876/_karma_webpack_/src/app/service/service-tesing/work-stub.service.ts:76:32)

定位到代码,是模拟服务层的这一行出现了问题:
图片.png

这个变量是这样定义的,换言之,只定义了类型,并没有初始化:
图片.png

所以,在undefined对象上调用wordId,当然会出错了。我恍然大悟。

然后另一个疑惑产生了,我的代码是粘过来的,为什么在之前的项目里没报错呢?
仔细的看了老项目,才发现我的错误源自于一个细微的改动。

老项目是这么写的:
图片.png

this.pageParamsCache = params;

把所有的参数直接赋值给pageParamsCache,不存在对它调用属性的问题。
而我为了方便,改成了:

this.pageParamsCache.workId = params.id;

这一改,就把原本的整体赋值,变成了赋值单个属性,但是pageParamsCache并没有初始化,所以才出现了undefined错误。

总结

无论初学什么技能,一定不要好高骛远,也不能自作主张,最重要的是遇到问题不要瞎猜,要按照合理的思维方式来思考。

青铜玩家冥思苦想半天都没有解决的问题,对于王者段位来说,就是两句话的事。这就是青铜和王者的差距。

为什么青铜玩家解决不了呢?因为我一开始的思考方向就错了。


LYX6666
1.6k 声望73 粉丝

一个正在茁壮成长的零基础小白