Unit testing a component that is getting data from Angular Behavior Subject.

huangapple go评论71阅读模式
英文:

Unit testing a compnent that is getting data from Angular Behavior Subject

问题

我有这个名为EmployeeService的服务,在这个服务中,我正在使用Angular的BehaviorSubject来从API获取数据。我从另一个组件中订阅了这个BehaviorSubject,现在我正在尝试测试它。但是因为我无法模拟上述服务中的数据,单元测试无法正常工作。

这是EmployeeService的基本结构:

class EmployeeService extends ApiService {
  public isEmployeeSub: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  someMethod() {
    this.isEmployeeSub.next(true);
  }

  // 其他方法
}

现在这是我要测试的组件。请注意,这是一个庞大的组件,但我只显示了最重要的部分,这是该组件工作所需的部分,即来自EmployeeServiceisEmployee数据。

class SalaryComponent implements OnInit, OnDestroy, AfterViewInit {
  private employeeService: EmployeeService;

  employee: EmployeeModel;
  employeeSubscription: Subscription;

  constructor(employeeService: EmployeeService) {
    this.employeeService = employeeService;

    this.employeeSubscription = this.employeeService.isEmployee.subscribe(isEmployee: boolean => {
      
      if (!isEmployee) {
        // 我希望这部分在某些测试中运行,而在其他测试中不运行。
        return;
      }

      // 进行一些其他操作
    });
}

上述组件正在订阅EmployeeService中的BehaviorSubject,如果它是员工,则获取数据并执行一些其他操作。如果不是isEmployee,则返回。我希望在某些测试中运行这个订阅中的条件,而在其他测试中不运行。

describe('SalaryComponent', () => {
  let component: SalaryComponent;
  let fixture: ComponentFixture<SalaryComponent>;
  let isEmployeeSubject = new BehaviorSubject<boolean>(false);
  let mockEmployeeService: jasmine.SpyObj<EmployeeService>;  

  beforeEach(async () => {
    mockEmployeeService = jasmine.createSpyObj<EmployeeService>(
      'EmployeeService', 
      [], 
      {
        isEmployeeSub: isEmployeeSubject
      }
    );

    await TestBed.configureTestingModule({
      imports: [],
      declarations: [],
      providers: [],
    })
      .compileComponents();
  });

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

  fit('should show drop down if it is employee', async ()  => {
     mockEmployeeService.isEmployeeSub.next(true);
  }

  fit('should not show drop down if it is employee', async ()  => {
     mockEmployeeService.isEmployeeSub.next(false);
  }
}

这是我以前使用ReplaySubject编写的测试方式。那个方法完全正常。但是这个测试似乎不起作用。一些原因使isEmployee的值始终为false。

英文:

I have this service called EmployeeService where I am using Angular's BehaviorSubject to get data from an api. I'm subcribing to this BehaviorSubject from another component which I am trying to test now. But because I'm unable to mock the data from the service mentioned above, the unit test is not working.

This is a bare bones of the EmployeeService:

class EmployeeService extends ApiService {
  public isEmployeeSub: BehaviorSubject&lt;boolean&gt; = new BehaviorSubject&lt;boolean&gt;(false);

  someMethod() {
    this.isEmployeeSub.next(true);
  }

  // other methods
}

Now this is the component I'm trying to test. Please note that this is a huge component but I'm just showing the most important part which is needed for this component to work and that is the isEmployee data coming from EmployeeService.

class SalaryComponent implements OnInit, OnDestroy, AfterViewInit {
  private employeeService: EmployeeService;

  employee: EmployeeModel;
  employeeSubscription: Subscription;

  constructor(employeeService: EmployeeService) {
    this.employeeService = employeeService;

    this.employeeSubscription = this.employeeService.isEmployee.subscribe(isEmployee: boolean =&gt; {
      
      if (!isEmployee) {
        // I want this part to run at some tests and not at the others.
        return;
      }

      // do some other things
    });
}

The above component is subscribing to BehaviorSubject in EmployeeService, gets the data if it an employee and runs some other things. If not isEmployee then it returns. I want that conditional in subscribe to run at some tests and not at the others.

describe(&#39;SalaryComponent&#39;, () =&gt; {
  let component: SalaryComponent;
  let fixture: ComponentFixture&lt;SalaryComponent&gt;;
  let isEmployeeSubject = new BehaviorSubject&lt;boolean&gt;(false);
  let mockEmployeeService: jasmine.SpyObj&lt;EmployeeService&gt;;  


  beforeEach(async () =&gt; {
    mockEmployeeService = jasmine.createSpyObj&lt;EmployeeService&gt;(
      &#39;EmployeeService&#39;, 
      [], 
      {
        isEmployeeSub: isEmployeeSubject
      }
    );

    await TestBed.configureTestingModule({
      imports: [],
      declarations: [],
      providers: [],
    })
      .compileComponents();
  });

  beforeEach(() =&gt; {
    fixture = TestBed.createComponent(SalaryComponent);
    component = fixture.componentInstance;
  });

  fit(&#39;should show drop down if it is employee&#39;, async ()  =&gt; {
     mockEmployeeService.isEmployeeSub.next(true);
  }

  fit(&#39;should not show drop down if it is employee&#39;, async ()  =&gt; {
     mockEmployeeService.isEmployeeSub.next(false);
  }
}

This is how I wrote a test previously with ReplaySubject. That works perfectly fine. But this one doesn't seem to work. The value isEmployee is always false somehow.

答案1

得分: 1

看起来你没有正确使用异步特性。
不要使用JavaScript中的async

最简单的方法是使用fakeAsync,如下所示:

fit('如果是员工应该显示下拉框', fakeAsync(() => {
  mockEmployeeService.isEmployeeSub.next(true);

  tick(); // 触发订阅
  expect(...).toBe(...);
});

fit('如果不是员工不应该显示下拉框', fakeAsync(() => {
  mockEmployeeService.isEmployeeSub.next(false);

  tick(); // 触发订阅
  expect(...).toBe(...);
});

第二个问题,你的isEmployeeSubject在所有的测试中都是共享的。因此,在上面的第二个测试中,发生的情况是isEmployeeSubject记住了上一个测试的true值。因此,在你的组件构造函数中,订阅首先会触发一个true
然后,你运行你的测试,执行.next(false),这将重新触发你的订阅。结果:你的订阅已经被触发了truefalse的值。
解决方法是在每个测试的beforeEach中重置BehaviorSubject

beforeEach(async () => {
  isEmployeeSubject = new BehaviorSubject<boolean>(false);
  mockEmployeeService = jasmine.createSpyObj<EmployeeService>(
    'EmployeeService',
    [],
    {
      isEmployeeSub: isEmployeeSubject
    }
  );
  // ...
});
英文:

It seems your not using the asynchronous features correctly.
Do not use the javascript async.

The easiest way is to use fakeAsync this way:

fit(&#39;should show drop down if it is employee&#39;, fakeAsync(()  =&gt; {
  mockEmployeeService.isEmployeeSub.next(true);

  tick(); // triggers subscribes
  expect(...).toBe(...);
});

fit(&#39;should not show drop down if it is employee&#39;, fakeAsync(()  =&gt; {
  mockEmployeeService.isEmployeeSub.next(false);

  tick(); // triggers subscribes
  expect(...).toBe(...);
});

2nd issue, your isEmployeeSubject is shared among all your tests. So in your second test from above, what happens is isEmployeeSubject remembers the true value from previous test. So in your component constructor, the subscribe first triggers with a true.
Then, you run your test, and do a .next(false), which re-triggers your subscribe. Result: your subscribe has both been triggered with true and false value.
Solution:
Reset the BehaviorSubject in your beforeEach:

beforeEach(async () =&gt; {
  isEmployeeSubject = new BehaviorSubject&lt;boolean&gt;(false);
  mockEmployeeService = jasmine.createSpyObj&lt;EmployeeService&gt;(
    &#39;EmployeeService&#39;, 
    [], 
    {
      isEmployeeSub: isEmployeeSubject
    }
  );
  // ...

huangapple
  • 本文由 发表于 2023年6月22日 19:45:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/76531548.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定