-
-
Notifications
You must be signed in to change notification settings - Fork 85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature Request: Allow auto mocking of methods returning Observable with custom Subject #2701
Comments
Thank you for the suggestion. The main problem here is that I think if we play with a helper function, it should work. ngMocks.stub$(apiService, 'save').next(data);
ngMocks.stub$(apiService, 'save').error(data); However, it requires to fetch Considering your example, why do you not want to use beforeEach(() => MockBuilder().mock(ApiService));
it('test', () => {
const subj = new Subject();
MockInstance(ApiService, 'save', () => subj);
const apiService = TestBed.inject(ApiService);
subj.next(data);
}); |
There are two reasons why I came up with my utility function:
Going back to my example: const deps = MockBuilder().mock(ApiService).build();
TestBed.configureTestingModule(deps);
const apiService = TestBed.inject(ApiService); You have pointed out the problem:
As a matter of fact, not only does it not know about Calling In order to get around the casting issue we could come up with a method on ngMocks object. Something like: ngMocks.injectMocked(ApiService) |
Agree, that's a good point, that's why I usually go with Something like: ngMocks.autoSpy('jasmine');
const deps = MockBuilder(UserService, ApiService).build();
TestBed.configureTestingModule(deps);
const userService = TestBed.inject(UserService); // real
const apiService = TestBed.inject(ApiService); // mock
// an override of a spy to return what I need
ngMocks.stubMember(apiService, 'loadConfig', () => of(...));
// calls apiService.loadConfig and apiService.userSave
userService.save();
// doesn't fail because apiService.post is a spy
expect(apiService.userSave).toHaveBeenCalledWith(...); Also, another thing is that it's not very clear whether Anyway for observables, it's just a Subject and I don't see a problem to inject it into any spy. We just need to find a good way to do it. |
right. This approach works for cases when you don't need to control the timing of the observable emitting. Recently I was writing a unit test where I actually needed to be in control of that. Regarding the spy from jasmine or jest... yeah tricky issue! Spectator solves it with separate import paths. Not sure if similar approach would be suitable for ng-mocks. It would certainly be a breaking change. |
Could you share that test? I would like to see a real use-case. |
I can't share exactly the use-case since it was work related. However, the approximate use-case is following:
The way I would test some of the behaviors is something like (pseudocode): // I kind of like this syntax with explicit `.keep()`, `.mock()` calls. It makes it very clear what's what
const deps = MockBuilder()
.keep(FormsModule)
.mock(SomeOtherComponent)
.mock(MatProgressBar)
.build();
// I use ng-mocks for mocking and spectator for testing environment
const createHost = createComponentFactory({
component: MyComponent,
...deps,
});
// SIFERS
function setup() {
// if the proposed feature was in place, I would not need to write this:
const apiService = createSpyFromClass(ApiService);
// if the proposed feature was in place, I would not need to write this:
const flush = createObservableFlushTriggers(
apiService,
'apiCall1',
'apiCall2',
'apiCall3'
);
const spectator = createHost({
// if the proposed feature was in place, I would not need to write this:
providers: [{ provide: ApiService, useValue: apiService }],
});
const pageObject = getPageObject(spectator);
return {
spectator,
flush,
apiService,
pageObject
};
}
function getPageObject(spectator: Spectator<MyComponent>) {
const selectFile = () => {
// fake implementation of selecting a file
spectator.detectChanges();
}
const pressCancel = () => spectator.click('button[aria-label="cancel"]');
return {
selectFile,
pressCancel
};
}
it('should be able to cancel request after second API call started', () => {
const { apiService, flush, pageObject } = setup();
pageObject.selectFile();
// only first API call started
expect(apiService.apiCall1).toHaveBeenCalled();
expect(apiService.apiCall2).not.toHaveBeenCalled();
// first API call returned data
flush.apiCall1.success({ /* some object */ });
// second API call started
expect(apiService.apiCall2).toHaveBeenCalled();
pageObject.pressCancel();
expect(pageObject.certainStateThatShouldBeTrueAfterCancel).toBe(true);
}); |
As a workaround for an issue of identifying all the methods that return an observable, we could perhaps use global configuration: ngMocks.defaultConfig(MyService, {
observablePropsToSpyOn: ['apiCall1', 'apiCall2', 'apiCall3'],
});
|
Hi there, that's a good idea. |
I am looking to do something similar to this feature request (I believe), but let me know if this is separate and/or unrelated and I can open a new ticket. I love this library, as it really speeds up the process of writing / maintaining tests! There's one thing I'm not sure how to accomplish though, as illustrated by the following example with Jasmine: const someServiceSpy = jasmine.createSpyObj('SomeService', ['someMethod']);
TestBed.configureTestingModule({
providers: [someServiceSpy],
// ...
});
const serviceToTest = TestBed.inject(ServiceToTest);
it('should call someMethod', () => {
serviceToTest.foo();
expect(someServiceSpy.someMethod)
.toHaveBeenCalled();
}); I really just want to make sure that the method Based on this comment above, it looks like this is the proposal for how this could be handled, but I'm just clarifying. |
Update: I ended up using a "hybrid" approach to accomplish this by creating a Jasmine const someServiceSpy: jasmine.SpyObj<SomeService>;
beforeEach(() => {
someServiceSpy = jasmine.createSpyObj('SomeService', ['someMethod']);
someServiceSpy.someMethod.and.returnValue('some-value');
return MockBuilder(ServiceToTest, TestModule)
.mock(SomeService, someServiceSpy);
});
it('should call someMethod', () => {
serviceToTest.foo();
expect(someServiceSpy.someMethod.calls.count())
.toBe(1);
}); |
Hi @kmjungersen, I think what you want is actually a combination of auto spy and Please let me know if it does what you expect. |
Hi @satanTime - appreciate you looking into this! I tried following that path and that works for checking to see if the method has been called, which is great. However the issue is when I attempt to actually stub a method for that service. Doing so makes the Mocked service function correctly, however then I extended the sandbox you started to illustrate this: https://codesandbox.io/s/practical-kowalevski-vsfxgb?file=/src/test.spec.ts. In reality |
Ah, I've got it! I didn't full understand how Here's an updated Sandbox that passes now: https://codesandbox.io/s/sad-feynman-dp8vq2?file=/src/test.spec.ts Thank you for pointing me in the right direction! |
Hi @kmjungersen, thanks for the info. With your requirements, I would simply create a spy instead of a stub function in MockBuilder(ServiceToTest, SomeService).mock(
SomeService,
{
someMethod: jasmine.createSpy().and.callFake((x: string) => x),
}
) Anyway, that's good that you've found a solution. |
I agree mocking observables can be impr0oved in ng-mocks. I read your current guide, but even the "permanent fix" is not good enough IMHO. Currently
ProblemAs said, and as you described in https://ng-mocks.sudo.eu/extra/mock-observables/ this is no good. The problem with the permannent fix is I still have to define this for each service or component that uses this. This is exactly what I want to avoid and why I use `ng-mocks´ in the first place - I want to avoid to define all the stubbing manually and want to use sensitive defaults. Proposed solutionSpeaking about defaults, why not just stub out every Observable with Something like: ngMocks.defaultMockOptions(() => ({
observables: EMPTY,
observableMethods: () => EMPTY,
})); I could still customize my stubs then or return something different, but it would be defaults that work/are enough in 99% of the cases. Even if you would have to assume each property ending in |
Describe the feature or problem you'd like to solve
I'd like to see if we can improve the Observable mocking story of ng-mocks.
The current solution of providing an override for a method/property where it would return a predefined observable is very limiting and I find myself never using this approach. Every time I tend to reach for the solution described in "Customizing observable streams" section. However, in order to minimize the amount of boilerplate, I came up with a function that automates the process. Here's the function (some types are not provided for the sake of brievety):
Here's how it's used:
Proposed solution
I'm wondering if something like above can be built into ng-mocks.
In the best case scenario it would work without any configuration if auto-spy was turned on. Here's how I imagine it:
Please let me know what you think of the feasibility of the idea.
Additional context
It looks like it's tricky to identify at run-time the return type of the method. So that's where it might get tricky to make this work without any configuration.
If I'm not mistaken, TypeScript does not quite support this out of the box. However, some libraries out there can possibly do it
The text was updated successfully, but these errors were encountered: