@boostercloud/application-tester
Version:
Contains Booster types related to the information extracted from the user project
111 lines (110 loc) • 5.41 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.fakeService = void 0;
const effect_1 = require("@boostercloud/framework-types/dist/effect");
/*
* This module exposed testing utilities for working with Effect services in tests.
*
* If you don't even know what Effect is, you probably should start by reading the docs
* in the `@boostercloud/framework-types/dist/effect` module.
*
* The key idea is that you can create a mock service that can be used in tests
* instead of the real service. This allows you to test your code without
* depending on the real service, nor having to replace functions of libraries with mocks.
*
* When testing services, we don't need to ensure that the service is actually calling
* the real implementation. We just need to ensure that the service is calling the
* right functions with the right arguments. That is, unless we're testing the live
* implementation, which is the only case where it is acceptable to replace the
* library functions with a mock.
*/
/**
* Main entry point of the Effect testing helper module.
* You pass the tag for your service, and a record of mocks for the functions. This function will
* return a helper object that contains a `Layer` that you can use to replace the service in your
* tests, the fakes record that you passed, and a `reset` function that will reset the history
* of all the fakes, so you can use them in multiple tests.
*
* A useful pattern is to create a `makeTestService` function that will create a new instance of
* the service for each test, passing the overrides you need for that test. This way, you can
* use the same `makeTestService` function in multiple tests, and you don't need to be passing all
* the fakes in every test.
*
* NOTE: This has a limitation where the service cannot define constants or values that are effects,
* as a workaround, you can define them as functions that return the value.
*
* E.g.:
* `() => Effect<...>` instead of `Effect<...>`
*
* and
*
* `() => Effect<..., T>` instead of `T`
*
* @example
* Here's the typical usage of this function (you can also check the tests in the @boostercloud/cli package):
* ```typescript
* import { fakeService, FakeOverrides } from '@boostercloud/application-tester/src/effect'
* import { fake } from 'sinon'
* import { MyService } from '../../../src/services/my-service'
*
* const makeTestMyService = (overrides?: FakeOverrides<MyService>) =>
* fakeService(MyService, {
* voidMethodInService: fake(),
* methodThatReturnsAValue: fake.returns(''),
* ...overrides,
* })
*
* // In your test:
* describe('my test', () => {
* it('should do something', async () => {
* const { layer, fakes, reset } = makeTestMyService()
*
* await unsafeRunEffect(functionThatUsesMyService(), {
* layer,
* onError: orDie
* })
*
* expect(fakes.methodThatReturnsAValue).to.have.been.calledWith('some value')
* reset() // You can also reset the fakes in an `afterEach` hook
* })
* })
* ```
*
* @param tag - The tag of the service to fake. E.g. `ProcessService`.
* @param fakes - A record of the methods to fake. E.g. `{ cwd: fake.returns(''), exec: fake.returns('') }`.
* @return {FakeServiceUtils} - An object with the layer to use in the dependency graph, and the fakes to assert the service was called with the right parameters.
*/
const fakeService = (tag, fakes) => {
// Assemble the fakes into a service that returns Effects in its functions.
// We disable the `any` warning because at this point the record is empty and TS will complain
// while we assemble it. The alternative was to use a `reduce` method, but the code became much more
// contrived, while not getting too much more type safety, as all the objects were of type unknown and we
// had to cast them explicitly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fakeService = {};
// Object.entries doesn't type properly the keys an values, it returns always string and unknown (yay!)
for (const [k, v] of Object.entries(fakes)) {
// We cast the value to a SinonSpy, as we know that's what we're passing in the `fakes` record.
// We don't really need to type the arguments or the return type, as that is typed by the return type
// of this function.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fakeFunction = v;
// We wrap the fake function in an Effect, so the layer can be used by the functions that require this
// service. We don't need to type the arguments or the return type, as that is typed by the return type.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fakeService[k] = (...args) => (0, effect_1.succeedWith)(() => fakeFunction(...args));
}
// Create a layer with that service as the only dependency, we can combine it with other layers using
// `Layer.all` in the tests.
const layer = effect_1.Layer.fromValue(tag)(fakeService);
// Create a reset function to reset all the fakes
const reset = () => {
for (const f of Object.values(fakes)) {
const fake = f;
fake.resetHistory();
}
};
// Return the layer, fakes, and reset function
return { layer, fakes, reset };
};
exports.fakeService = fakeService;