Skip to content

Instantly share code, notes, and snippets.

@mattapperson
Created June 29, 2019 20:52
Show Gist options
  • Save mattapperson/7b32d0260b3661c4c217285018212390 to your computer and use it in GitHub Desktop.
Save mattapperson/7b32d0260b3661c4c217285018212390 to your computer and use it in GitHub Desktop.

Automocking thoughts

Allow users to create mocks using 2 APIs... Consumers or providors could write mocks using these tools...

// This block only runs if recording live
await ifLiveBlock(async () => {
  await new Promise(resolve => {
    const pipe = spawn("mongod", ["--dbpath=<LOCATION>", "--port", "1223"]);
    pipe.stdout.on("data", function(data) {
      if (data.indexOf("started") !== -1) {
        resolve();
      }
    });
  });
});

// if CLI flag / env var is set to record, this returns the live server results
// if not, it returns a snapshotted version. When live the above ifLiveBlock would have
// started the server it would connect to making the process seamless
const results = await memorized("Mocked query for bar", async () => {
  const client = await mongodb.connect({ port: 1223 });
  return client.query({ foo: "bar" });
});

// In the above call to memorizeMock we have a name, a function that conditionaly called
// if we are "recording" or the snapshotted response if playing back.

Pros:

  • one test/mock file, and one step... No need to write a test that generates the mocks first, allowing it to feel a lot more like Jest snapshots.
  • This not only allows for tests to be run as unit tests... but if the CI flag is on also as intigration tests. All with a single test being written!
  • No setup to ensure is working localy or a CI... enviorment starting or not is not programaticly controlled in the test itself

That's the API... in practice it would look more like this:

///////////////////////
// user.test.js
///////////////////////
test("user creation works", () => {
  const user = require("../user");
  return user.create("mitchell", 22);
});

///////////////////////
// ./__mocks__/user.js
///////////////////////
const user = jest.genMockFromModule("../user");

// If the tests using this mock are live, this runs, otherwise it does not
await ifLiveBlock(async () => {
  await new Promise(resolve => {
    const pipe = spawn("mongod", ["--dbpath=<LOCATION>", "--port", "1223"]);
    pipe.stdout.on("data", function(data) {
      if (data.indexOf("started") !== -1) {
        resolve();
      }
    });
  });
});

// If live, this returns data from the live mongodb server,
// otherwise it returns from the memorized data saved to a snapshot
user.create = (name, age) => {
  return await memorized(`create-user`,
  async () => {
    const mongodb = require('mongodb');
    const client = await mongodb.connect({ port: 1223 });
    return await client.insert({ name, age });
  }, [...args]
  // ˆˆ matches all args to the snapshot data
);
};

module.exports = user;

If memorized/ifLiveBlock use an env var to toggle live vs mocked data, then this becomes Test framework agnostic.

This API/setup also means I can make use of all the advantages of this regardless of if I am the providor OR the consumer of the mocks, meaning I don't need to get buy-in from the entire team or company to use it.

@shlomokraus
Copy link

shlomokraus commented Jun 30, 2019

The ifLiveBlock is something that works exactly the same. I will use it for initiation logic and it is a good idea for many other cases.

So if I translate it into Mockshot, it will be almost the same.

// user.test.ts

// The test that creates\validate the mock 

test("user create", async () => {
  const result = user.create("Some One", 21);  
  // Regular assertions checking values
  expect(result.name).toEqual("Some One");
  expect(result.age).toEqual(21);
  // Assertion checking shape 
  expect(result).toMatchMock(UserService, "create", "success"); 
  // ^ This part creates the mock and also asserts against it. Notice that I've added 
  // a mock name "success", this is because every method usually have different types of mock results. 
  // You may also have a mock for when user doesn't exists or if it failed.
});

Now Mockshot will use AST to generate an actual class and create mocks/UserService.ts that looks something like this:

export class UserServiceMock {
    static create(mock: "success"): any {
        switch (mock) {
            case "success":
                return {
                  "name": "Some One",
                  "age": 18
                 }
            default:
                throw Error("Unknown mock: "+mock);
        }
    }
}

That means, that I can take this and simply merge it with the real class using isLiveBlock.
There are actually many ways to do it cleanly, it is easy to do automatically and pretty cool:

// set user service mock as default
let userService = new UserServiceMock();

// override the mock with live instance if needed
ifLiveBlock(()=>{
  mongoClient.init(); // connect to db etc...
  userService = new UserService(mongoClient);
});

console.log(userService);
// *** Now we have "userService" which is either a mock or the actual instance with all the initiation logic ran. 

@shlomokraus
Copy link

Notice that I didn't add any overhead to what someone else would do.
There is one test for "UserService.create", and another test that uses it.
The only overhead is adding "toMatchMock" on the original test. That is enough to generate
and validate the mock.
toMatchMock is just like toMatchSnapshot, only that it saves the snapshot in a way that allows it
to be deserialized easily into a class structure.

So the hard part of generating, validating and syncing the mock is done. Now you can wrap and use it any way you want, like with the isLiveBlock.

That said, I use IoC containers and usually have a single point of initialization logic in the test, so it is
easy to wrap that point in isLiveBlock, which is something that I actually would implement now :)

@mattapperson
Copy link
Author

Yes, but this would still require one of the following:

  1. enough buy-in from the provider to use toMatchMock.
  2. write my own test that uses toMatchMock.

Or am I missing something?

If I am not missing something, this is a major issue.

Based on your blogs I think this is both correct and an intended “feature”... it just does not work well for all use-cases... including mine.

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