Add `useSmartResource` in Jest specs
What does this MR do?
This MR introduces a useSmartResource
utility (and a related useComponent
and useAxiosMockAdapter
) which significantly help simplify specs by managing test resources similar to how RSpec does with let(...)
.
Example
Here's a dummy example of using this utility:
import { useSmartResource, useFactoryArgs, useComponent } from 'helpers/resources';
describe('silly spec', () => {
const [store] = useSmartResource(createStore);
const [wrapper] = useComponent((props = {}) => shallowMount(MyComponent, { propsData: props, store }));
describe('with flag 2', () => {
useFactoryArgs(wrapper, { flag: 2 });
// Now, whenever wrapper is created it will pass these arguments to the original factory
// but only in this describe context
it('does things', () => {
// Note that we don't need to explicitly createWrapper!
expect(wrapper.text()).toContain('I have 2 flags');
});
});
it('works', () => {
expect(wrapper.text()).toBe('');
});
});
How does this work?
The main components at play are:
type FactoryFunction<T> = (...args: any[]) => T;
type TeardownFunction<T> = (instance: T) => void;
interface SmartResource<T> {
constructor(factory: FactoryFunction<T>, teardown: TeardownFunction<T>);
/**
* Returns the underlying instance which this resource is managing.
*
* Will trigger `create()` if it has to.
*/
get instance(): T;
/**
* Creates the instance using the current factory function and the given args.
*
* Will throw an error if instance tries to be created twice.
*/
create(...args: any[]);
/**
* Runs the teardown function on the given instance and removes reference to it.
*/
teardown();
}
/**
* This function creates a SmartResource hooked into the `afterEach` and returns a Proxy that points to the instance the SmartResource manages.
*
* - The Proxy will accept the same props that T expects.
* - The underlying instance **will not** be created until the proxy is triggered.
* - If the underlying instance needs to be created without using the proxy, a connected factory function is provided as
* the second part of the return's tuple.
*/
useSmartResource<T>(factory: FactoryFunction<T>, teardown: TeardownFunction<T>): [Proxy<T>, (...args: any[]) => void]
/**
* This uses `beforeAll` and `afterAll` to replace in-scope the factory function of the SmartResource powering the given Proxy.
*/
useFactoryArgs<T>(resource: Proxy<T>, ...args: any[]);
useSmartResource
?
SD: What happens when I call sequenceDiagram
participant A as test_spec.js
participant B as helpers/resources
participant C as SmartResource
participant D as SmartResourceProxy
participant E as Jest
A->>B: useSmartResource(factory, teardown)
B->>C: new(factory, teardown)
C-->>B: smartResource
B->>D: new(smartResource, SmartResourceProxyHandler)
D-->>B: proxy
B->>E: afterEach(() => smartResource.teardown())
E-->>B:
B-->>A: [proxy, (...args) => smartResource.create(...args)]
SD: What happens when I grab a prop from a proxy?
sequenceDiagram
participant A as test_spec.js
participant B as SmartResourceProxy
participant C as SmartResourceProxyHandler
participant D as SmartResource
A->>B: proxy.foo
B->>C: handler.get(resource, 'foo')
Note right of C: SmartResource will create the instance if necessary
C->>D: resource.instance['foo']
D-->>C: val
C-->>B: val
B-->>A: val
useFactoryArgs
?
SD: What happens when I call sequenceDiagram
participant A as test_spec.js
participant B as helpers/resource
participant C as helpers/resource
participant D as Jest
A->>B: useFactoryArgs(proxy, fooArg)
B->>B: newFactory = origFactory => () => origFactory(fooArg)
B->>C: useFactory(proxy, newFactory)
C->>C: resource = unbox(proxy)
Note right of C: beforeAll and afterAll only run in the current `describe` scope
C->>D: beforeAll(saveOrigFactory(resource))
D-->>C:
C->>D: beforeAll(replaceFactory(resource, newFactory))
D-->>C:
C->>D: afterAll(restoreOrigFactory(resource))
Edited by Paul Slaughter