Strict typing in lax tests
For more than 10 years, _big__ of my work is pure JavaScript. But sometimes you have to deal with slightly more typed languages. And every time after such contact there remains a strange feeling of "incomprehensibility", why in js one cannot do the same.
Especially because you can. And today we’ll try to add a little typing to the most chaotic area of javascript - mock in unit tests.
And then everyone here says that mocking is a coding smell .
What are the main arguments against mok? In short:
And if in Russian -
There is only one problem - nodejs is an IoC framework, and the modular system in its current execution is DI. In principle, this is why popular libraries for mokas - proxyquire, mockery and others, generally work in principle.
That is, purely technically - mocking is not a coding smell.
But there is another side to the issue - the libraries themselves "smell" so much that they infect the rest of the code a bit. Proxyquire is just crooked, mockery is too slow, mokas in jest are especially beautiful. I gave some overview of this in a May article about rewiremock .
The "correctness" of what is moka, what of test, is very easy to determine - it must fall correctly, and must fall in the right place. If the mock is crooked, the mock should fall, not expect (someFunction) .to.be.called (). After all, sometimes you can wonder for so long why the method is not called when everything should be fine.
There was a good command in proxyquire - callThrough, which allows you to use the "real" file as the basis. But they didn’t recommend using it, because it is difficult to control which exports were locked up and which were not. It’s not even paid attention to the fact that it is difficult to control the entire file process.
Jest has a very convenient __mock__ automatic mok system that allows you to transparently provide mok for real files ... in a slightly complicated way. Or all or nothing. Well, no one controls these mokas.
The main problem of mok is very easy to describe with one wildest example from tslint , the no-export-by-default rule:
In the case of mok, it will be even more fun - the tests will remain green, since they will work with the mok, which both worked and works, but the real file - the real file can do anything - it does not exist for the tests .
This is the main problem, and the main coding smell from mocks. Well, they do not add confidence in the future.
I myself faced this situation a couple of times, and on the third I decided to just close it technically. More specifically, rewiremock v3 supports static typing of mocks for Flow / JS and shallow export checks for regular JS.
So - let's start with an example.
Proxyquire does exactly what it says - it overloads dependencies. But he does not check what he is actually doing there - that fs will be requested, that the function is intercepted, that the operation is valid.
Mockery is even worse, since the announcement of mok and their use is a little spaced out in space.
In rewiremock, it’s not at all better. Given that the core API is roughly repeating the mockery interface.
This is a common way to create a mok. Does he care if there is a default export for the file, and whether there is an export named helper, which is also a number. As described above, the discrepancy between the mok and the real file, which it should replace, is the problem, and there is the main source of “smell”.
There are two
solutions to this situation: The first uses dynamic imports that return promises for TS / Flow, information from which can be used in the future exclusively through static typing. The underlying code is only required to support the ability to set the name of the import function. Hi asynchrony :(
Moreover - all the necessary information is available for IntelliSence or another car compiler. Pure TypeScript in action (well, or Flow). This not only makes moki “better”, but also improves the “user experience” when writing moki. IDE will help and prompt.
The second option is simpler, but it can only work in runtime, including it can be easily ported to almost any other library, since there are literally three lines of code.
In this case, before the operation of replacing the real file with a mock (somewhere deep in require), the real file will actually be loaded, and its exports will be compared with this mock for matching type names.
The main plus of this option is that it works synchronously - in the end, working with this is convenient, while using dynamic imports requires the use of asynchronous APIs, which is not very convenient :(
PS: there are problems with the “typing” of the good old synchronous require, so don't . even try
I'll be honest - the only thing that improves typing mokov - mokov the time of writing (if typing) and finding the bugs (in both cases) I was just teoriticheki whether to do it interesting, you can, in principle, and I basically did..
Jest is so good that he even has his own mox system, sealed in his sandbox of file execution (which is one of the main features).
This system is two-faced, and consists of __mocks__ automocks, and manual mocks. The former are beautiful, the latter are terrible. Well, in the guts of Jest, everything is bad and not entirely logical - it replaces the node.js modular system with itself, and babel and everything else, filling all the space and destroying the entire "standard" infrastructure. For many, this is a fact - proxyquire does not work with Jest. Nothing works with Jest.
But I would like to have typed moki, and there is a solution.
This is a small cli utility jest-typed-mock , which scans directories in search of mok and checks for compliance with real files.
Operation Mode 4:
I started it with a couple of examples, and I was able to find various "accumulated" discrepancies in moks and religions that were on the way to "tests are green, but no food," but (thank God) they have not reached yet. Honestly - I would like to see such a system built into Jest by default.
For reference: rewiremock is arrogant enough to push Jest and dab yourself. To do this, you need to perform only two actions -
This completely kills jest for those files that will be requested - no sandboxing, jest as a local variable, of course, no jest mocks, especially __mocks__ ... But the order for this was received.
For almost a year now I've been trying to saw rewiremock. It started as a proxyquire replacement, continued as a mockery replacement, and sank somewhere further.
Every time I drink coffee in the shadow of eucalyptus, I think - what new feature to add, and why. Then I understand that this is not possible ... and I get my way in a couple of months.
So it was with isolation modes, with work under webpack, jest, and now with typing. Well, if you want something from% username% from a small library for mokas - you just know who you can contact.
More on the link .
Especially because you can. And today we’ll try to add a little typing to the most chaotic area of javascript - mock in unit tests.
And then everyone here says that mocking is a coding smell .
"Not" the right mokas
What are the main arguments against mok? In short:
Mocking is required when our decomposition strategy has failed.
- as Eric Elliot said in the link above.
And if in Russian -
- Your hands are crooked
- Use DI!
- Moki - they are not honest
There is only one problem - nodejs is an IoC framework, and the modular system in its current execution is DI. In principle, this is why popular libraries for mokas - proxyquire, mockery and others, generally work in principle.
That is, purely technically - mocking is not a coding smell.
But there is another side to the issue - the libraries themselves "smell" so much that they infect the rest of the code a bit. Proxyquire is just crooked, mockery is too slow, mokas in jest are especially beautiful. I gave some overview of this in a May article about rewiremock .
"The right mokee"
The "correctness" of what is moka, what of test, is very easy to determine - it must fall correctly, and must fall in the right place. If the mock is crooked, the mock should fall, not expect (someFunction) .to.be.called (). After all, sometimes you can wonder for so long why the method is not called when everything should be fine.
There was a good command in proxyquire - callThrough, which allows you to use the "real" file as the basis. But they didn’t recommend using it, because it is difficult to control which exports were locked up and which were not. It’s not even paid attention to the fact that it is difficult to control the entire file process.
Jest has a very convenient __mock__ automatic mok system that allows you to transparently provide mok for real files ... in a slightly complicated way. Or all or nothing. Well, no one controls these mokas.
The main problem of mok is very easy to describe with one wildest example from tslint , the no-export-by-default rule:
// app.ts
import ActionPopover from "./action";
// action.ts
export default class ActionMenu {
...
}
// oops, we just renamed this from popover -> menu but our consumer had no idea!
In the case of mok, it will be even more fun - the tests will remain green, since they will work with the mok, which both worked and works, but the real file - the real file can do anything - it does not exist for the tests .
This is the main problem, and the main coding smell from mocks. Well, they do not add confidence in the future.
Type safety
I myself faced this situation a couple of times, and on the third I decided to just close it technically. More specifically, rewiremock v3 supports static typing of mocks for Flow / JS and shallow export checks for regular JS.
TypeScript vs Flow?
It took me about a week to write 10 lines of TS / Flow that would do all the work, while 90% of the time I spent on Flow, and another week to write 10 more lines, while 90% of the time I spent on TS. Friendship won.
So - let's start with an example.
var foo = proxyquire('./foo', {
'fs': { fileWroteSync }
});
Proxyquire does exactly what it says - it overloads dependencies. But he does not check what he is actually doing there - that fs will be requested, that the function is intercepted, that the operation is valid.
Mockery is even worse, since the announcement of mok and their use is a little spaced out in space.
mockery.registerMock('fs', fsMock);
mockery.enable();
const mocked = require(...)
In rewiremock, it’s not at all better. Given that the core API is roughly repeating the mockery interface.
// Старый API - требует имя файла строчкой. Пиши/мокай что угодно
rewiremock('./b.js').withDefault(overrideDefault).with({ helper:2});
This is a common way to create a mok. Does he care if there is a default export for the file, and whether there is an export named helper, which is also a number. As described above, the discrepancy between the mok and the real file, which it should replace, is the problem, and there is the main source of “smell”.
There are two
solutions to this situation: The first uses dynamic imports that return promises for TS / Flow, information from which can be used in the future exclusively through static typing. The underlying code is only required to support the ability to set the name of the import function. Hi asynchrony :(
// Новый API - использует import, что решает много проблем с name-resolution
rewiremock(() => import('./b.js'))
.withDefault(overrideDefault) // метода default может просто не быть
.with({ helper: 42 }) // c этой проверкой все тоже хоршо
Moreover - all the necessary information is available for IntelliSence or another car compiler. Pure TypeScript in action (well, or Flow). This not only makes moki “better”, but also improves the “user experience” when writing moki. IDE will help and prompt.
The second option is simpler, but it can only work in runtime, including it can be easily ported to almost any other library, since there are literally three lines of code.
rewiremock('./b.js')
.with({ helper: 42 })
.toMatchOrigin(); // дополнительная опция
In this case, before the operation of replacing the real file with a mock (somewhere deep in require), the real file will actually be loaded, and its exports will be compared with this mock for matching type names.
The main plus of this option is that it works synchronously - in the end, working with this is convenient, while using dynamic imports requires the use of asynchronous APIs, which is not very convenient :(
PS: there are problems with the “typing” of the good old synchronous require, so don't . even try
I'll be honest - the only thing that improves typing mokov - mokov the time of writing (if typing) and finding the bugs (in both cases) I was just teoriticheki whether to do it interesting, you can, in principle, and I basically did..
Jest?
Jest is so good that he even has his own mox system, sealed in his sandbox of file execution (which is one of the main features).
This system is two-faced, and consists of __mocks__ automocks, and manual mocks. The former are beautiful, the latter are terrible. Well, in the guts of Jest, everything is bad and not entirely logical - it replaces the node.js modular system with itself, and babel and everything else, filling all the space and destroying the entire "standard" infrastructure. For many, this is a fact - proxyquire does not work with Jest. Nothing works with Jest.
But I would like to have typed moki, and there is a solution.
This is a small cli utility jest-typed-mock , which scans directories in search of mok and checks for compliance with real files.
Operation Mode 4:
- jest-typed-mock flow, for Flow
- jest-typed-mock typescript, for TS
- jest-typed-mock javascript, for "strict" JS checking (compares the number of arguments in functions)
- jest-typed-mock exports, to check only names and types.
I started it with a couple of examples, and I was able to find various "accumulated" discrepancies in moks and religions that were on the way to "tests are green, but no food," but (thank God) they have not reached yet. Honestly - I would like to see such a system built into Jest by default.
For reference: rewiremock is arrogant enough to push Jest and dab yourself. To do this, you need to perform only two actions -
// 1. Восстановить связь между модулями.
// Иначе "родителем" rewiremock будет jest-runtime
rewiremock.overrideEntryPoint(module);
// 2. Переписать require на "нормальный"
require = rewiremock.requireActual;
This completely kills jest for those files that will be requested - no sandboxing, jest as a local variable, of course, no jest mocks, especially __mocks__ ... But the order for this was received.
Conclusion
For almost a year now I've been trying to saw rewiremock. It started as a proxyquire replacement, continued as a mockery replacement, and sank somewhere further.
Every time I drink coffee in the shadow of eucalyptus, I think - what new feature to add, and why. Then I understand that this is not possible ... and I get my way in a couple of months.
So it was with isolation modes, with work under webpack, jest, and now with typing. Well, if you want something from% username% from a small library for mokas - you just know who you can contact.
More on the link .