Skip to content

Add `useSmartResource` in Jest specs

Paul Slaughter requested to merge ps-smart-resource-in-tests into master

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[]);

SD: What happens when I call useSmartResource?

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

SD: What happens when I call useFactoryArgs?

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

Merge request reports