Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: move closer to test double definitions #150

Merged
merged 38 commits into from
Apr 14, 2022

Conversation

crookse
Copy link
Member

@crookse crookse commented Apr 11, 2022

Overview of commits

  • feat: add Fake()
  • feat: add Dummy()
  • feat: new mock methods (expects(), method(), verifyExpectations())
  • feat: pre-programmed method implementation for faking/mocking methods
  • feat: add interfaces so that they can be returned for improved type hinting
  • refactor: mocks to use mixin
  • refactor!: stubs (no need to stub a class to stub its members)
  • refactor: types are now stricter and provide better type hinting
  • test: test fakes, dummies, mocks, stubs and move tests files into unit/mod

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:

  • Mocks
  • Dummies
  • Fakes
  • Stubs (breaking changes made in this PR)

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

@crookse crookse changed the title feat: new mock methods feat: move closer to test double definitions Apr 12, 2022
@crookse crookse force-pushed the feat/new-mock-methods branch from a14ef67 to 05c66d2 Compare April 12, 2022 03:19
@ebebbington ebebbington added the Type: Minor Merging this pull request results in a minor version increment label Apr 12, 2022
Copy link
Member

@ebebbington ebebbington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omg this is beautiful eric, usual minor things but my god the code is sexy

README.md Outdated
<img src="https://img.shields.io/badge/Tutorials-YouTube-red">
</a>
</p>
# Drash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cough

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HAHA dang.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doneski: cc0ebdf

extend.js Outdated
// deno-lint-ignore-file
// This code was bundled using `deno bundle` and it's not recommended to edit it manually

class Base {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wha dis doing here ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL test files

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

donezo: 92bd978

extend.ts Outdated
@@ -0,0 +1,9 @@
class Base {
public base_prop = "base prop";
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, test file you committed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dooonnnne: 92bd978

Comment on lines 56 to 67
//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

/**
* @param original - The original object to mock.
* @param methodsToTrack - The original object's method to make trackable.
*/
public init(original: OriginalObject, methodsToTrack: string[]) {
this.#original = original;
this.#calls = this.#constructCallsProperty(methodsToTrack);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constructor needs to be updated with methods - public

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually that is below so maybe rm that comment block and move method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved them stuffs: 08e47ef

@crookse crookse requested a review from Guergeiro April 13, 2022 00:28
@crookse crookse changed the title feat: move closer to test double definitions feat!: move closer to test double definitions Apr 13, 2022
@crookse crookse merged commit d3fbffe into breaking-remove-runner Apr 14, 2022
@crookse crookse deleted the feat/new-mock-methods branch April 14, 2022 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Minor Merging this pull request results in a minor version increment
Development

Successfully merging this pull request may close these issues.

2 participants