Skip to content

Commit

Permalink
fix: an example how to handle "TypeError: Cannot read property 'subsc…
Browse files Browse the repository at this point in the history
…ribe' of undefined"

closes #226
  • Loading branch information
satanTime committed Nov 5, 2020
1 parent f5ee1bc commit 70d5a25
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 1 deletion.
115 changes: 114 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2481,6 +2481,119 @@ feel free to [contact us](#find-an-issue-or-have-a-question-or-a-request) if you

### How to fix `TypeError: Cannot read property 'subscribe' of undefined`

This issue means that something has been mocked and returns a dummy result (`undefined`) instead of observable streams.

For example, if we have `TodoService.list$()`,
that returns a type of `Observable<Array<Todo>>`,
and a component,
that fetches the list in `OnInit` via `subscribe` method:

```typescript
class TodoComponent implements OnInit {
public list: Observable<Array<Todo>>;

constructor(protected service: TodoService) {}

ngOnInit(): void {
// Never do like that.
// It is just for the demonstration purposes.
this.service.list$().subscribe(list => (this.list = list));
}
}
```

If we wanted to test the component, we would like to mock its dependencies. In our case it is `TodoService`.

```typescript
TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [MockProvider(TodoService)],
});
```

If we created a fixture, we would face the error. This happens because a mocked copy of `TodoService.list$`
returns a spy, if [auto spy](#auto-spy) has been configured, or `undefined`. Therefore, neither has the `subscribe` property.

Obviously, to solve this, we need to get the method to return an observable stream.
For that, we could create a mocked copy via [`MockService`](#how-to-mock-a-service) and to pass it as the second parameter into [`MockProvider`](#how-to-mock-a-provider).

```typescript
const todoServiceMock = MockService(TodoService);
ngMocks.stub(todoServiceMock, {
list$: () => EMPTY,
});

TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [MockProvider(TodoService, todoServiceMock)],
});
```

Profit, now initialization of the component with the mocked service does not throw the error anymore.

Nevertheless, usually, we want not only to stub the result with an `EMPTY` observable stream,
but also to provide a fake subject, that would simulate its calls.

A possible solution is to create a context variable of `Subject` type for that.

```typescript
let todoServiceList$: Subject<any>; // <- a context variable.

beforeEach(() => {
todoServiceList$ = new Subject(); // <- create the subject.
const todoServiceMock = MockService(TodoService);
ngMocks.stub(todoServiceMock, {
list$: () => todoServiceList$,
});

TestBed.configureTestingModule({
declarations: [TodoComponent],
providers: [MockProvider(TodoService, todoServiceMock)],
});
});

it('test', () => {
const fixture = TestBed.createComponent(TodoComponent);
// Let's simulate emits.
todoServiceList$.next([]);
// Here we can do some assertions.
});
```

A solution for [`MockBuilder`](#mockbuilder) is quite similar.

```typescript
let todoServiceList$: Subject<any>; // <- a context variable.

beforeEach(() => {
todoServiceList$ = new Subject(); // <- create the subject.
const todoServiceMock = MockService(TodoService);
ngMocks.stub(todoServiceMock, {
list$: () => todoServiceList$,
});

return MockBuilder(TodoComponent).mock(
TodoService,
todoServiceMock
);
});

it('test', () => {
const fixture = MockRender(TodoComponent);
todoServiceList$.next([]);
// some assertions.
});
```

This all might be implemented with [`MockInstance`](#mockinstance) too,
but it goes beyond the topic.

The source file is here:
[MockObservable](https://github.com/ike18t/ng-mocks/blob/master/examples/MockObservable/test.spec.ts).<br>
Prefix it with `fdescribe` or `fit` on
[codesandbox.io](https://codesandbox.io/s/github/satanTime/ng-mocks-cs?file=/src/examples/MockObservable/test.spec.ts)
to play with.

[to the top](#content)

---
Expand All @@ -2507,7 +2620,7 @@ TestBed.configureTestingModule({
The problem is clear: when we mock the module, [`MockModule`](#how-to-mock-a-module) recursively mocks its dependencies, and, therefore, it creates a mocked copy of `SharedModule`.
Now imported and mocked declarations are part of 2 modules.

To solve it we need to let [`MockModule`](#how-to-mock-a-module) know, that `SharedModule` should not be mocked.
To solve this, we need to let [`MockModule`](#how-to-mock-a-module) know, that `SharedModule` should not be mocked.

There are good and bad news. Bad news is that [`MockModule`](#how-to-mock-a-module) does not support that, but good news is that `ngMocks` has [`MockBuilder`](#mockbuilder) for such a complicated case.
The only problem now is to rewrite `beforeEach` to use [`MockBuilder`](#mockbuilder) instead of [`MockModule`](#how-to-mock-a-module).
Expand Down
83 changes: 83 additions & 0 deletions examples/MockObservable/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { CommonModule } from '@angular/common';
import { Component, Injectable, NgModule } from '@angular/core';
import { MockBuilder, MockInstance, MockRender, ngMocks } from 'ng-mocks';
import { Observable, Subject } from 'rxjs';

// A simple service, might have contained more logic,
// but it is redundant for the test demonstration.
@Injectable()
class TargetService {
public readonly value$: Observable<number[]> = new Observable();
}

@Component({
selector: 'target',
template: `{{ list | json }}`,
})
class TargetComponent {
public list: number[];

constructor(service: TargetService) {
service.value$.subscribe(list => (this.list = list));
}
}

@NgModule({
declarations: [TargetComponent],
imports: [CommonModule],
providers: [TargetService],
})
class TargetModule {}

describe('MockObservable', () => {
// Because we want to test the component, we pass it as the first
// parameter of MockBuilder. To mock its dependencies we pass its
// module as the second parameter.
beforeEach(() => MockBuilder(TargetComponent, TargetModule));

// Now we need to customize the mocked copy of the service.
// value$ is our access point to the stream.
const value$: Subject<number[]> = new Subject();
beforeAll(() => {
// MockInstance helps to override mocked instances.
MockInstance(TargetService, {
init: instance =>
ngMocks.stub(instance, {
value$, // even it is a read-only property we can override it in a type-safe way.
}),
});
});

// Cleanup after tests.
afterAll(() => {
value$.complete();
});

it('has access to the service via a component', () => {
// Let's render the component.
const fixture = MockRender(TargetComponent);

// We haven't emitted anything yet, let's check the template.
expect(fixture.nativeElement.innerHTML).not.toContain('1');
expect(fixture.nativeElement.innerHTML).not.toContain('2');
expect(fixture.nativeElement.innerHTML).not.toContain('3');

// Let's simulate an emit.
value$.next([1, 2, 3]);
fixture.detectChanges();

// The template should contain the emitted numbers.
expect(fixture.nativeElement.innerHTML).toContain('1');
expect(fixture.nativeElement.innerHTML).toContain('2');
expect(fixture.nativeElement.innerHTML).toContain('3');

// Let's simulate an emit.
value$.next([]);
fixture.detectChanges();

// The numbers should disappear.
expect(fixture.nativeElement.innerHTML).not.toContain('1');
expect(fixture.nativeElement.innerHTML).not.toContain('2');
expect(fixture.nativeElement.innerHTML).not.toContain('3');
});
});

0 comments on commit 70d5a25

Please sign in to comment.