-
-
Notifications
You must be signed in to change notification settings - Fork 775
Description
Describe the bug
Restoring stubs created by createStubInstance creates frankenstein objects that have the original prototype-chain of the object passed in without the object having been properly created through the constructor.
To Reproduce
Here is a jsfiddle demonstrating the following code
class TestObject {
constructor() {
this.constructorDidRun = true;
}
doSomething() {
if (!this.constructorDidRun) {
throw new Error("I am Frankenstein's monster!!")
}
}
}
const sandbox = sinon.createSandbox();
const stubInstance = sandbox.createStubInstance(TestObject);
stubInstance.doSomething(); // does nothing, as expected
sandbox.restore();
stubInstance.doSomething(); // uh-oh!Expected behavior
I'm not totally certain, but perhaps nothing? For us, the use-case of createStubInstance is to create drop-in objects that simulate the provided object's interface, but without ever touching its implementation. We use them in unit-testing to validate interactions. I'm not sure the use-case where you'd want a partial object that has not had its constructor run, so I cannot imagine this is intended behavior (and even in that case, I would expect to expose that behavior using stub.callThrough(), rather than by restoring the sandbox).
Context
- Library version: 14.0.2
- Environment: Node/browser
- Example URL: https://jsfiddle.net/dpraul/4atk8jdv/2
Additional Context
This behavior came to our attention as we're updating our application from Angular 13 to Angular 14. Angular 14 updates the default TestBed behavior to tear down the test module after each test. While this is arguably a good addition as it provides a cleaner testing environment for each test, it has brought these frankenstein objects to our attention.
Since a sandox is created and restored inside of a test-suite, but the TestBed teardown occurs after the suite, the TestBed still holds references to these partial objects. The TestBed cleanup triggers any OnDestroy methods on the objects, which may now have implementations associated with them.
NOTE: This particular issue may be better-solved a multitude of ways (perhaps by introducing the TestBed teardown into the afterEach of the suite and prior to restoring the sandbox). Nonetheless, it seems like an issue that the implementations of these stubbed objects are being reintroduced back to the test suite, even after the suite has completed.
Here is an example of this issue in our application
import * as sinon from "sinon";
import { of } from "rxjs";
import { Injectable, OnDestroy } from "@angular/core";
import { TestBed } from "@angular/core/testing";
@Injectable({ providedIn: "root" })
class Dependency implements OnDestroy {
private subscription = of(null).subscribe();
ngOnDestroy() {
// It's common to use the OnDestroy lifecycle hook to clean-up any RxJS subscriptions.
// The sandbox is restored in afterEach, which re-introduces this implementation,
// but since the constructor has never been called this.subscription will be undefined, so a TypeError is thrown
this.subscription.unsubscribe();
}
}
@Injectable({ providedIn: "root" })
class ServiceUnderTest {
constructor(private dependency: Dependency) {}
doSomething(): boolean {
return true;
}
}
describe("Test Suite", () => {
let service: ServiceUnderTest;
let sandbox: sinon.SinonSandbox;
let mockDependency: sinon.SinonStubbedInstance<Dependency>;
beforeEach(() => {
sandbox = sinon.createSandbox();
mockDependency = sandbox.createStubInstance(Dependency);
TestBed.configureTestingModule({
providers: [
ServiceUnderTest,
{ provide: Dependency, useValue: mockDependency }
]
});
service = TestBed.inject(ServiceUnderTest);
});
afterEach(() => {
sandbox.restore();
});
it("should do something", () => {
expect(service.doSomething()).toBe(true);
});
});