Skip to content

Instantly share code, notes, and snippets.

@DanielMSchmidt
Last active December 10, 2018 13:00
Show Gist options
  • Save DanielMSchmidt/a6fb42ae798f019be34a27a3c325a388 to your computer and use it in GitHub Desktop.
Save DanielMSchmidt/a6fb42ae798f019be34a27a3c325a388 to your computer and use it in GitHub Desktop.
This is a rough draft of how we could define mocks in a nice way

API Proposal

I would like to solve the problem of stubs in a way that allows us to easily extend them. I would also like to prevent the stubs to go stale and I would like to have it defined in a central place. Let me show you what I have in mind on the next files.

const Cluster = require("#mocks/definitions/cluster");
export function DefaultCluster({ children, applications, clusterProps = {} }) {
const hostname = "127.12.12.4";
return (
<Cluster {...clusterProps}>
<Node hostname={hostname}>
{/* My point is we COULD use mechanisms like this, we don't need to. It empowers us IMHO */}
{React.Children.map(applications(), element =>
React.cloneElement(element, { hostname })
)}
</Node>
{children}
</Cluster>
);
}
export function SingleNodeCluster({ children }) {
return <DefaultCluster applications={() => children} />;
}
export function TwoNodeCluster({
firstNodeApplications,
secondNodeApplications
}) {
return (
<DefaultCluster applications={firstNodeApplications}>
<Node hostname="127.12.12.5">
{React.Children.map(secondNodeApplications(), element =>
React.cloneElement(element, { hostname })
)}
</Node>
</DefaultCluster>
);
}
const { setStub, setStubWithAdapter } = require("magic-lib");
const DefaultCluster = require("#mocks/default-cluster");
const DefaultApp = require("#mocks/default-app");
describe("Service", () => {
it("has a started service", () => {
const stub = setStub(
<DefaultCluster
renderApplications={() => <DefaultApp healthStatus="healthy" />}
/>
)
);
});
it("runs on different services on different nodes", () => {
const stub = setStub(
<DefaultCluster
renderApplications={() => <DefaultApp healthStatus="healthy" />}
>
{/* This could also be another import if you use it multiple times */}
<Node hostname="8.8.8.8">
<Application id="/my-other-service">
<Task status="running" />
</Application>
</Node>
</DefaultCluster>
)
);
// ... assertions
});
it("runs same service on different nodes in different zones", () => {
/* Although this seems like a lot of code we could leverage code reuse here.
What I would like to highlight here is we have the possibility to clearly define
what we need from a mock in a descriptive way and fine-grained. A first time reader
will immediately understand which cluster we currently have in our mock. The magic is
how it happens, not what happens.
*/
const stub = setStub(
<Cluster>
<Node hostname="9.9.9.9" region="us-west-1" zone="us-west-1a">
<Application id="/my-service">
<Task status="running" />
</Application>
</Node>
<Node hostname="8.8.8.8" region="eu-west-3" zone="eu-west-3c">
<Application id="/my-service">
<Task status="running" />
</Application>
</Node>
</Cluster>
);
// ...
});
});
describe("Possible further extensions", () => {
// I would like to show you how we could extend this solution in crazy ways.
// Do we want this? I don't know, but I would love to be enabled to do it if I choose to want it.
it("could have callbacks", () => {
const mockApplicationCreation = jest.fn();
const stub = setStub(
({ withApp }) => (
<DefaultCluster
renderApplications={() =>
withApp ? <DefaultApp healthStatus={healthStatus} /> : null
}
clusterProps={{
onAppCreate: mockApplicationCreation
}}
/>
),
{
withApp: false
}
);
// Do action that "creates" an app
expect(mockApplicationCreation).toHaveBeeenCalled();
mockApplicationCreation.resetMocks();
stub.setProps({
withApp: true
});
// Do action that "creates" an app, should not send a request because the app already exists
expect(mockApplicationCreation).not.toHaveBeeenCalled();
});
it("could have adapters", () => {
const setStubForMockServer = setStubWithAdapter("mockserver");
const setStubForDyson = setStubWithAdapter("dyson");
const setStubForUnicornSolutionThatSolvesAllOurProblems = setStubWithAdapter(
"unicorn"
);
});
it("could have a dynamic approach to changes", () => {
const stub = setStub(
({ healthStatus }) => (
<DefaultCluster
renderApplications={() => <DefaultApp healthStatus={healthStatus} />}
/>
),
{
healthStatus: "healthy"
}
);
// Cypress assertion that something is like it should be
stub.setProp({ healthStatus: "unhealthy" });
// Cypress assertion that something is like it should be
stub.reset();
});
});

Evaluation

Pros

  • We can keep up with new functionality of our services by extending the Cluster component with some additional mocks
  • We can easily define defaults in a dynamic and extensible way without having too much magic in place
  • We can define different renderers / adapters for different mock servers (mock-server / dyson / ...)
  • The solution is very extensible in a lot of directions (making it more dynamic, ...)
  • We have one central place to keep up with changes of the APIs we consume not 20, therefore our stubs are not so likely to go stale.
  • We can choose the granularity on which we want our mocks to be defined. We can start with very little props and more "scenario"-like outputs and move step by step to the granularity we need

Cons

  • JSX always gives the notion of being flexible / changing. It is meant here as only being a static way to describe the current state in the very same file as the tests
  • A little bit of magic is involved by this layer of abstraction, at least on how it happens

Implementation

We would need to write a react-renderer. There are a lot of examples that we could take a look at. There is a step by step guide how to do it. If we can find a good abstraction as a basis for defining stubs for different routes as a react element we only need to translate to API calls to the mock server.

@nLight
Copy link

nLight commented Mar 29, 2018

Looks like a misuse of JSX to me. Maybe XML is what you want :)
Obviously you have 5 pros and 1 con. I'd challenge you to think what if it doesn't make sense?

@DanielMSchmidt
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment