drashland / rhum

A test double library
https://drash.land/rhum
MIT License
92 stars 6 forks source link

feat!: move closer to test double definitions #150

Closed crookse closed 2 years ago

crookse commented 2 years ago

Overview of commits

Updated README

See the new README here.

Refactor and add features to be closer to test double definitions

Test double definitions can be found here: https://martinfowler.com/bliki/TestDouble.html

Currently, we only have:

Still need to add in spies.

Stub()

Before, we created a stub via Stub(someObject) and then we could call someObject.stub(...). This didn't make sense per definition. The definition is:

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

We shouldn't have to call stub on an object to make it stub-able. Instead we should call Stub(theObjectReceivingTheStub, theDataMemberToStub, optionalValueTheStubShouldReturn). API looks like the below now:

class Server {
  public greeting = "hello";

  public methodThatLogs() {
    console.log("Server running.");
  }
}

const server = new Server();

Stub(server, "greeting", "you got changed");
assertEquals(server.greeting, "you got changed");

Stub(server, "greeting");
assertEquals(server.greeting, null);

// `is_stubbed` should be added when stubbing an object
assertEquals("is_stubbed" in server, true);

Use mixin for mock

Introduces using a mixin to create a mock so that calling private methods and returning this also works in a mock. For example, the old mock implementation wouldn't work when someComplexMethod() below would return this and call this.#setSomethingOne() and this.#setSomethingTwo() because the context of this was the mock object and not the original object. It needs to be an instance of the original object. That's where the mixin comes in. Basically, mocks are now extensions of the original so that mock instanceof original === true. This approach also fixes the getter/setter issue from https://github.com/drashland/rhum/pull/148/files.

class TestObjectFourBuilder {
  #something_one?: string;
  #something_two?: string;
  ...
  ...
  ...
  someComplexMethod(): this {
    this.#setSomethingOne();
    this.#setSomethingTwo();
    return this;
  }
  ...
  ...
  ...
}

Add mock.method(...).willReturn(...)

class TestObjectThree {
  public test(): string { return "World"; }
}

const mock = new MockBuilder(TestObjectThree).create();
assertEquals(mock.is_mock, true);

// Original returns "World"
assertEquals(mock.test(), "World");

// Don't fully pre-program the method. This should cause an error during
// assertions.
mock
  .method("test")
  .willReturn({
    name: "something"
  });

assertEquals(mock.test(), {name: "something"});
assertEquals(mock.calls.test, 2);
assertEquals(mock.calls.hello, 2);

Add mock.method(...).willThrow(...)

class TestObjectThree {
  public test(): string { return "World"; }
}

const mock = new MockBuilder(TestObjectThree).create();
assertEquals(mock.is_mock, true);

// Original returns "World"
assertEquals(mock.test(), "World");

// Make the original method throw RandomError
mock
  .method("test")
  .willThrow(new RandomError("Random error message."))

assertThrows(
  () => mock.test(),
  RandomError,
  "Random error message."
);
assertEquals(mock.calls.test, 2);

Add mock.expects(...).toBeCalled(...)

const mock = new MockBuilder(TestObjectThree).create();
assertEquals(mock.is_mock, true);

mock.expects("hello").toBeCalled(2);

mock.test(); // Calls .hello() under the hood twice

mock.verifyExpectations(); // Must be called to verify the .expects() calls

Add Fake()

Fakes kind of act like mocks, but they do not have calls and expectations like mocks.

// Assert that a fake can make a class take a shortcut
const fakeServiceDoingShortcut = Fake(Repository).create();
fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut");
const resourceWithShortcut = new Resource(
  fakeServiceDoingShortcut,
);
resourceWithShortcut.getUsers();
assertEquals(fakeServiceDoingShortcut.anotha_one_called, false);
assertEquals(fakeServiceDoingShortcut.do_something_called, false);
assertEquals(fakeServiceDoingShortcut.do_something_else_called, false);

// Assert that the fake service is not yet doing a shortcut
const fakeServiceNotDoingShortcut = Fake(Repository).create();
const resourceWithoutShortcut = new Resource(
  fakeServiceNotDoingShortcut,
);
resourceWithoutShortcut.getUsers();
assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true);
assertEquals(fakeServiceNotDoingShortcut.do_something_called, true);
assertEquals(fakeServiceNotDoingShortcut.do_something_else_called, true);

Add Dummy()

Dummies are empty objects of an instance of something. For example, Dummy(Hello) instanceof Hello === true. This makes it a little bit easier to fill in parameter lists where parameters must be instances of something.

const mockServiceOne = Mock(ServiceOne).create();
const dummy3 = Dummy(ServiceThree);

const resource = new Resource(
  mockServiceOne,
  Dummy(ServiceTwo),
  dummy3,
);

resource.callServiceOne();
assertEquals(mockServiceOne.calls.methodServiceOne, 1);

Dummies can also be created without having to specify constructor arguments.

class Hello {
  constructor(
    arg1: SomethingOne,
    arg2: SomethingTwo,
    arg3: SomethingThree,
  ) {
    ...
    ...
    ...
  }
}

const dummy = Dummy(Hello); // works without throwing error