ts-mockito
ts-mockito copied to clipboard
Unexpected behavior when partial mocking.
Suppose I have a class with two methods: methodA
and methodB
. Suppose methodB
delegates to methodA
. Finally, suppose that I have set up a spy on an instance of the class that will modify the behavior of methodA
, but leaves methodB
alone.
I would expect that when methodB
is invoked, it will get the mocked behavior for methodA
.
Here is an example in Java that demonstrates the behavior I would expect:
class TestClass {
public String getMessage() {
return "real message";
};
public String delegatingMethod() {
return this.getMessage();
};
}
public class SpyTest {
@Test
public void test() {
TestClass testClass = new TestClass();
TestClass spy = Mockito.spy(testClass);
System.out.println("BEFORE MOCKING: testClass.getMessage()=" + spy.getMessage());
System.out.println("BEFORE MOCKING: testClass.delegatingMethod()=" + spy.delegatingMethod());
doReturn("spy value").when(spy).getMessage();
System.out.println("AFTER MOCKING: testClass.getMessage()=" + spy.getMessage());
System.out.println("AFTER MOCKING: testClass.delegatingMethod()=" + spy.delegatingMethod());
}
}
the following lines are output:
BEFORE MOCKING: testClass.getMessage()=real message
BEFORE MOCKING: testClass.delegatingMethod()=real message
AFTER MOCKING: testClass.getMessage()=spy value
AFTER MOCKING: testClass.delegatingMethod()=spy value
When I try something similar in typescript, my code doesn't even run:
class TestClass {
getMessage = () => {
return 'real message';
};
delegatingMethod = () => {
return this.getMessage();
};
}
describe('Test partial mock', () => {
it('test', () => {
const testClass = new TestClass();
console.log(`*** BEFORE MOCKING: testClass.getMessage()=${testClass.getMessage()}`);
console.log(`*** BEFORE MOCKING: testClass.delegatingMethod()=${testClass.delegatingMethod()}`);
const spyTestClass = spy(testClass);
when(spyTestClass.getMessage()).thenReturn('mocked message');
console.log(`*** AFTER MOCKING: testClass.getMessage()=${testClass.getMessage()}`);
console.log(`*** AFTER MOCKING: testClass.delegatingMethod()=${testClass.delegatingMethod()}`);
})
});
The following shows up in my log:
*** BEFORE MOCKING: testClass.getMessage()=real message
*** BEFORE MOCKING: testClass.delegatingMethod()=real message
*** AFTER MOCKING: testClass.getMessage()=mocked message
TypeError: Cannot read property 'get' of undefined
at Spy.getEmptyMethodStub (node_modules/ts-mockito/src/Spy.ts:34:50)
at Spy.Mocker.getMethodStub (node_modules/ts-mockito/src/Mock.ts:242:25)
at TestClass.delegatingMethod (node_modules/ts-mockito/src/Mock.ts:161:37)
at Context.it (tests/DbSupportSdkTest.ts:37:78)
So It gets to where the spied version of delegatingMethod()
is called, and fails.
While experimenting, I tried moving the spy declaration to the beginning of the test (without doing any stubbing), I would expect the first two calls to execute according to their original implementations. However, instead I get the same error as above, just sooner.
Here is the updated code:
describe('Test partial mock', () => {
it('test', () => {
const testClass = new TestClass();
const spyTestClass = spy(testClass);
console.log(`*** BEFORE MOCKING: testClass.getMessage()=${testClass.getMessage()}`);
console.log(`*** BEFORE MOCKING: testClass.delegatingMethod()=${testClass.delegatingMethod()}`);
when(spyTestClass.getMessage()).thenReturn('mocked message');
console.log(`*** AFTER MOCKING: testClass.getMessage()=${testClass.getMessage()}`);
console.log(`*** AFTER MOCKING: testClass.delegatingMethod()=${testClass.delegatingMethod()}`);
})
})
In the log, I get no log output, just the error message:
TypeError: Cannot read property 'get' of undefined
at Spy.getEmptyMethodStub (node_modules/ts-mockito/src/Spy.ts:34:50)
at Spy.Mocker.getMethodStub (node_modules/ts-mockito/src/Mock.ts:242:25)
at TestClass.getMessage (node_modules/ts-mockito/src/Mock.ts:161:37)
at Context.it (tests/DbSupportSdkTest.ts:30:73)
This seems to imply that when I spy on a class, any method that hasn't had its behavior stubbed will give me an error. That doesn't seem like it could possibly be right, and the examples in your README
seem to imply otherwise. I'm hoping I'm missing something obvious.
For reference, here are my dependencies from package.json
:
"dependencies": {
"@types/aws-lambda": "^8.10.13",
"@types/jsonwebtoken": "^8.3.3",
"@types/yesql": "^3.2.1",
"aws-sdk": "^2.544.0",
"jsonwebtoken": "^8.5.1",
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.6",
"chai": "^4.2.0",
"mocha": "^6.0.2",
"ts-mockito": "^2.5.0",
"ts-node": "^8.0.2",
"typescript": "^3.3.3333"
}
And, in case it matters, my tsconfig.json
:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"strict": true,
"typeRoots": [
"node_modules/@types"
],
"types": [
"node"
],
"esModuleInterop": true
},
"exclude": [
"**/tests"
]
}
Hi @besk-cerity, since you don't have class methods but class properties holding functions, you need to mock the property.
Could you please try like this?:
when(spyTestClass.getMessage).thenReturn('mocked')
Thanks for the quick response.
That actually doesn't compile:
TS2345: Argument of type '"mocked message"' is not assignable to parameter of type '() => string'.
Instead I tried:
const spyTestClass = spy(testClass);
when(spyTestClass.getMessage).thenReturn(() => 'mocked message');
Now when I run the test, It fails with the message:
TypeError: testClass.getMessage is not a function
at Context.it (tests/DbSupportSdkTest.ts:35:72)
Your suggestion makes sense, but in my example I was able to get the mocked version of getMessage()
to behave correctly. It was the other method (that didn't have any stubbing) that was causing problems. What I was seeing is that if I spied on a class and then tried call a method on it that hadn't been stubbed, the method invocation failed with TypeError: Cannot read property 'get' of undefined
.
it does seem to be a function declaration issue though.
I rewrote the class as follows:
class TestClass {
getMessage() {
return 'real message';
};
delegatingMethod() {
return this.getMessage();
};
}
Now when I run the test:
describe('Test partial mock', () => {
it('test', () => {
const testClass = new TestClass();
const spyTestClass = spy(testClass);
console.log(`*** BEFORE MOCKING: testClass.getMessage()=${testClass.getMessage()}`);
console.log(`*** BEFORE MOCKING: testClass.delegatingMethod()=${testClass.delegatingMethod()}`);
when(spyTestClass.getMessage()).thenReturn('mocked message');
console.log(`*** AFTER MOCKING: testClass.getMessage()=${testClass.getMessage()}`);
console.log(`*** AFTER MOCKING: testClass.delegatingMethod()=${testClass.delegatingMethod()}`);
})
});
I get the following output.
*** BEFORE MOCKING: testClass.getMessage()=real message
*** BEFORE MOCKING: testClass.delegatingMethod()=real message
*** AFTER MOCKING: testClass.getMessage()=mocked message
*** AFTER MOCKING: testClass.delegatingMethod()=mocked message
Any idea how I can work around this issue using arrow functions. Sometimes the context calls for them. Plus, I'd prefer not to rewrite a bunch of classes because there was a possibility I might partially mock it.
Sure, Tomorrow I'll check and I let you know
Sorry for the late response @besk-cerity. For those partially mocked objects, Spy
is inspecting the object's property descriptor, and evaluating getters and setters. Since arrow function doesn't have it, then the issue comes.
I don't know if this could be tackled in another fashion or not. I need to investigate further to check.
Thanks for continuing to investigate. I'm surprised this hadn't come up before.