Angular 6单元测试:在afterAll中抛出错误\nReferenceError:找不到变量:$ thrown

73

当运行我的单元测试时,有时即使它们通过了,但在所有测试运行结束时,我仍会收到以下错误。

在我的 Jenkins CI 构建中运行 PhantomJS 时:

.PhantomJS 2.1.1 (Linux 0.0.0) ERROR
  {
    "message": "An error was thrown in afterAll\nReferenceError: Can't find variable: $ thrown",
    "str": "An error was thrown in afterAll\nReferenceError: Can't find variable: $ thrown"
  }

或者在 Chrome 上:

Chrome 67.0.3396 (Windows 7 0.0.0) ERROR
  {
    "message": "An error was thrown in afterAll\n[object ErrorEvent] thrown",
    "str": "An error was thrown in afterAll\n[object ErrorEvent] thrown"
  }

我也有非常不可靠的测试,有时什么都不改变,它们会成功,而其他时候相同的测试会失败,所以我知道一些奇怪的事情正在发生。

17个回答

115

我的问题是由于测试设置的愚蠢方式导致了竞态条件,但我仍希望在这里记录下来,因为我很难在互联网上找到解决方案。

我所做的事情是声明了两个beforeEach函数来设置我的测试,并且其中一个是异步的,所以有时它们会按顺序失败。

以下是我的测试样例:

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

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

因此,为了解决这个问题,我将所有的设置放在一个同步的beforeEach中。

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [HomeComponent]
    }).compileComponents();
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

我浪费了太多时间试图弄清楚这个问题,所以我把它放在这里以便帮助其他人。


18
现在你是否存在一种竞态条件,即在创建组件时编译组件不一定已经完成?“非常愚蠢的方法”是指CLI生成的内容,被称为“典型方法” - jonrsharpe
我不是在说它对还是错(虽然我倾向于错误),但文档说你所做的是正确的,因为 compileComponents 返回一个 Promise。https://angular.io/guide/testing#calling-compilecomponents - Todd Sjolander
3
要改进这个答案,请参考官方Angular测试指南中的Calling compileComponents()部分。简而言之,如果您正在使用ng test(我希望许多人都在使用),则调用compileComponents()是不必要的,因为ng CLI已经为我们编译了组件。因此,不需要调用此方法,也不需要将async()传递给beforeEach()。因此,在整个测试套件中,我们可以获得同步调用而没有任何竞争条件。 - Mladen
1
调用 compileComponents() 的目的是使测试可以在没有 CLI 的情况下运行,这在 CI 环境中非常常见。 - Mozgor

60

我遇到了类似的问题。自从 Angular v6 和 Karma v3 之后,这个模糊的 afterAll 错误开始出现 (https://github.com/jasmine/jasmine/issues/1523)。对我来说,这个错误没有导致测试失败,但却导致了套件失败。

在查看了许多解决方案之后,发现造成这个问题的原因几乎总是不同的,这使得在线寻求帮助变得困难。我们可以期待补丁更新在某个时候添加更好的错误信息。

我的错误

[INFO] HeadlessChrome 71.0.3542 (Linux 0.0.0) DialogComponent #apply should save. FAILED
[INFO]  Uncaught TypeError: Cannot read property 'nativeElement' of undefined thrown
[INFO]       [31m✗ [39m[31mshould save.[39m
[INFO]  Uncaught TypeError: Cannot read property 'nativeElement' of undefined thrown
[INFO] 
[INFO] HeadlessChrome 71.0.3542 (Linux 0.0.0) DialogComponent #apply should save. FAILED
[INFO]  Uncaught TypeError: Cannot read property 'nativeElement' of undefined thrown
[INFO] HeadlessChrome 71.0.3542 (Linux 0.0.0) DialogComponent #apply should save. FAILED
[INFO]  Uncaught TypeError: Cannot read property 'nativeElement' of undefined thrown
[INFO] HeadlessChrome 71.0.3542 (Linux 0.0.0) ERROR
[INFO]   {
[INFO]     "message": "An error was thrown in afterAll\nUncaught TypeError: Cannot read property 'nativeElement' of undefined thrown\nUncaught TypeError: Cannot read property 'nativeElement' of undefined thrown",
[INFO]     "str": "An error was thrown in afterAll\nUncaught TypeError: Cannot read property 'nativeElement' of undefined thrown\nUncaught TypeError: Cannot read property 'nativeElement' of undefined thrown"
[INFO]   }

查找出错的测试

我收到了一个afterAll错误信息,但不知道是什么导致它出现,或者哪个测试触发了它。 我首先安装了karma-spec-reporter: npm install karma-spec-reporter --save-dev

将其添加到karma.conf.js文件的插件数组中: require('karma-spec-reporter') 这样就可以使用spec报告器,并将spec添加到报告器数组中:reporters: ['spec'],

下次运行该测试时,您将在有问题的测试后在控制台中看到afterAll错误。

我的问题 / 解决方案

我发现测试调用了htmlElement.click()。 我将其更改为:htmlElement.dispatchEvent(new Event('click)) 然后测试开始通过了。

总结

作为一般准则,我现在避免在HTMLElement上使用.click()。 此外,当用户与UI交互时,应使用事件来模拟用户的操作,这始终是测试时的好习惯。


7
插件karma-spec-reporter非常方便,它会在测试运行之前输出规范文件的名称,这样就更容易看出哪些测试导致了问题。 - user9903
3
你的意思是在你的回答中把 require('karma-spec-reporter') 添加到句子“将它添加到 karma.conf.js 文件中的插件数组中:”的末尾吗?如果没有这个 "require" 部分(也就是说,你只是尝试将 'spec' 添加到 reporters 数组中),你会收到一个错误。 - Gregg L

49

另一个帮助我解决问题的方法是,在每个测试后添加销毁 fixture 语句。

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


1
这对我也有效。在 GitHub Actions 上遇到了这个问题。 - IdiakosE Sunday
非常感谢!我遇到了类似的错误:在 afterAll 中抛出了一个错误 失败:无法解构未定义的“profile”属性。- 这就是解决方案!干杯! - eyesfree
天啊,终于成功了!谢谢。我一直在不同的组件中随机地遇到 afterAll Failed: SyntaxErrors 的问题。 - Glare Storm
这对我也起作用了!但我不明白为什么...也许是一些悬空变量留在内存中?我本以为 karma 每次都会完全拆除测试? - ProxyTech
非常感谢!你帮我省了很多时间! - undefined
显示剩余2条评论

31

我们遇到了类似的问题,都出现了间歇性错误和测试失败的情况。在升级到Angular 6后,我们也升级了Jasmine 3,其中随机运行测试是默认设置。

正如 @marsraits 在下面所指出的那样,这种变化是这些错误的根源,意味着我们的测试不正确地共享了依赖关系,因此真正的错误源头在于我们自己的代码。话虽如此,我们并没有重写大量旧的测试,而是通过在测试运行器中关闭随机选项来快速解决了问题。

我们通过在 karma.conf.js 中添加以下设置来实现:

  config.set({
    client: {
      jasmine: {
        random: false
      }
    }
  })

55
通常当测试有时失败有时成功时,这表明测试之间存在依赖关系,应该隔离开来。设置"random: false"只是掩盖了这个问题。正确的解决方法是找到根本原因并消除依赖关系。 - martsraits
4
我完全同意上面@martsraits的观点,但当你有一个庞大而不稳定的测试套件且你的CI/CD构建会间歇性失败时,这个答案可以拯救情况。这给了你时间去解决根本原因。 - ynovytskyy
在我的情况下也是同样的问题,object ErrorEvent没有堆栈跟踪,在每次运行中以不同的测试用例随机失败,我无法调试它,而且只会在PhantomJS中发生,而不会在Chrome或ChromeHeadless中发生。 - Matias Fernandez Martinez
1
我遇到的问题是没有提供明确的回溯信息,无法找到测试失败的源头。这几乎是不可能调试的。 - user9903
所有的观点都很好。我已经更新了我的答案,以更好地反映反馈意见。 - dibbledeedoo

29

当发生此错误时,请检查karma打开的浏览器并检查其控制台是否有错误。

通常会在那里看到堆栈跟踪,这将帮助您解决问题。这也适用于由karma引发的其他不具信息性的错误。


1
使用Chrome Headless,这是可能的吗? - Erik
1
Erik:不过你可以使用Chrome进行调试,然后再切换回来,对吧? - Norbert Huurnink
2
这确实帮助我找到了问题所在,我能够进行纠正。谢谢。 - JP Roussel
3
这真的救了我的一天。这是找出罪犯的唯一方法。 - Alfabravo

4

我刚刚遇到了这个问题。我的问题与HttpClient有关。 我模拟了一个httpClient返回错误503,但我没有在subscribe中创建错误函数:

  httpClient.get('/api').subscribe(() => {
      fail('Should not return success');
    }, (data) => {
      expect(data.status).toEqual(503); // without adding this callback here, you will get the error of this question
    });
    const request = mock.expectOne('/api');

    request.flush({data: 'test'}, { status: 503, statusText: 'Internal Error'});

感谢您,我遇到了同样的问题,我没有在订阅中创建错误函数。 - wael jawadi

4

非常感谢 Oisin,因为他的答案指引了我正确的方向。请把这个答案看作是对他回答的补充。

然而,我认为有两个方面需要进一步澄清:

  1. 我们没有处理竞态条件(race condition)。


    竞态条件会意味着两个 beforeEach 实例同时运行,我们无法确定哪一个先结束。实际上,非异步的 beforeEach 先运行,而异步的则后运行。每一次都是如此。
    当你有两个 beforeEach 且其中一个是异步的时候(包括使用 @angular/core/testing 提供的闪亮的 waitForAsync 封装器的情况),异步实例的执行会被推到执行队列的末尾。

  2. 我也觉得 Oisin 提出的解决方案:

[...] 把所有的设置放在一个同步的 beforeEach 中。

…太过于限制性了。它可以是异步的而不会有问题。

重要的部分是,TestBed.createComponent() 应该在 TestBed.configureTestingModule() 已经解决后运行。
就是这样。

为了让它更加清晰明了,这里有一个随机的例子:

import { TestBed, waitForAsync } from '@angular/core/testing';
// more imports...

describe('SomeComponent', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [SomeComponent],
      imports: [SharedModule, RouterTestingModule, HttpClientTestingModule],
      providers: [{
        provide: SomeService, useValue: {
          someObservableMethod$: () => of()
        } as Partial<SomeService>
      }]
    })
      .overrideModule(MatIconModule, MatIconModuleMock)
      .compileComponents();
  }));

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

  /* tests here */
});

...应该被转化为:

import { TestBed, waitForAsync } from '@angular/core/testing';
// more imports...

describe('SomeComponent', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [SomeComponent],
      imports: [SharedModule, RouterTestingModule, HttpClientTestingModule],
      providers: [{
        provide: SomeService, useValue: {
          someObservableMethod$: () => of()
        } as Partial<SomeService>
      }]
    })
      .overrideModule(MatIconModule, MatIconModuleMock)
      .compileComponents();

    fixture = TestBed.createComponent(SomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  /* tests here */
});

第二个(同步的)beforeEach 的代码被添加到了第一个(异步的)beforeEach 中。就是这样。

谢谢。我不得不改变一百万个测试,但这肯定是处理它的正确方法。 - Ε Г И І И О

3
我的这个错误问题是由于没有模拟我正在测试的组件的子组件所致。在这种情况下,我有一个主页组件和两个子组件,这些子组件需要声明,并且我没有模拟它们。
因此,子组件具有真实的依赖关系,这会间歇性地导致测试以这种不明显的方式失败(看起来好像不同的测试随机失败,但事实并非如此)。
在这种情况下,以下模拟方法效果很好:
@Component({
    selector: 'app-exercise',
    template: '<p>Mock Exercise Component</p>'
})
class MockExerciseComponent {
}

@Component({
    selector: 'app-user',
    template: '<p>Mock User Component</p>'
})
class MockUserComponent {
}

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

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            // note you need to mock sub components!
            declarations: [HomepageComponent, MockExerciseComponent, MockUserComponent],

5
在你的TestBed配置中使用schemas: [NO_ERRORS_SCHEMA]比模拟子组件更好。这将忽略它无法解析的任何子组件。这将把你的组件与其子组件分离开来,以便你可以进行隔离测试。 - jkyoutsey
谢谢@jkyoutsey - 很有用 - Richard
7
实际上,我不再赞同使用NO_ERRORS_SCHEMA。它会隐藏太多错误。这是我的当前建议:https://medium.com/@fivedicephoto/why-you-shouldnt-use-no-errors-schema-in-angular-unit-tests-cdd478c30782 - jkyoutsey

2

我也遇到了类似的问题。在我的Angular 10项目中,有10次中有8次会出现afterall错误。结果发现,在spec.ts文件中,应该使用HttpClientTestingModule而不是HttpClientModule进行导入。尝试一下这个方法,可能会解决Afterall问题中的随机错误。


1
在我的情况下,问题似乎是缺少“捕获”机制。不仅如此,还需要:

this.service.getUsers().subscribe( r => { doSomething... } );

我需要做:

this.service.getUsers().subscribe( r => { doSomething... }, err => { doSomething... }  );

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接