Skip to main content
fixed spelling, minor clarification
Source Link
hoffmale
  • 777
  • 5
  • 10
  • A fake is an object that behaves the same things as the original and is to a certain extent indistinguishable. Its code can normally be reused across tests. This can, for example, be a simple in-memory database for a database wrapper.
  • A stub just implements as much as needed to fulfill the required operations of this test. In most cases, a stub is specific to a test or a group of tests requiring only a small set of the methods of the original. In this example, it could be a dataStore that just implements a suitable version of insertUserRecord() and recordIpAddress().
  • A mock is an object that lets you verify how it is used (most often by letting you evaluate calls to its methods). I'd try to use them sparelysparingly in unit tests since by using them you actually try to test the function implementation and not the adherence to its interface, but they still have their uses. Many mock frameworks exists to help you create just the mock you need.

Note that if any of these methods throws an error, we want the error to bubble up to the calling code, so that it can handle the error as it sees fit. If it's being called by the API code, it may translate the error into an appropriate httpHTTP response code. If it's being called by a web interface, it may translate the error into an appropriate message to be displayed to the user, and so on. The point is this function doesn't know how to handle the errors that may be thrown.

For an example, consider someone adding a call to oven.preheat() (optimization!) in youyour cookie baking example:

  • A fake is an object that behaves the same things as the original and is to a certain extent indistinguishable. Its code can normally be reused across tests. This can, for example, be a simple in-memory database for a database wrapper.
  • A stub just implements as much as needed to fulfill the required operations of this test. In most cases, a stub is specific to a test or a group of tests requiring only a small set of the methods of the original. In this example, it could be a dataStore that just implements a suitable version of insertUserRecord() and recordIpAddress().
  • A mock is an object that lets you verify how it is used (most often by letting you evaluate calls to its methods). I'd try to use them sparely in unit tests since you actually try to test the function implementation and not the adherence to its interface, but they still have their uses. Many mock frameworks exists to help you create just the mock you need.

Note that if any of these methods throws an error, we want the error to bubble up to the calling code, so that it can handle the error as it sees fit. If it's being called by the API code, it may translate the error into an appropriate http response code. If it's being called by a web interface, it may translate the error into an appropriate message to be displayed to the user, and so on. The point is this function doesn't know how to handle the errors that may be thrown.

For an example, consider someone adding a call to oven.preheat() (optimization!) in you cookie baking example:

  • A fake is an object that behaves the same as the original and is to a certain extent indistinguishable. Its code can normally be reused across tests. This can, for example, be a simple in-memory database for a database wrapper.
  • A stub just implements as much as needed to fulfill the required operations of this test. In most cases, a stub is specific to a test or a group of tests requiring only a small set of the methods of the original. In this example, it could be a dataStore that just implements a suitable version of insertUserRecord() and recordIpAddress().
  • A mock is an object that lets you verify how it is used (most often by letting you evaluate calls to its methods). I'd try to use them sparingly in unit tests since by using them you actually try to test the function implementation and not the adherence to its interface, but they still have their uses. Many mock frameworks exists to help you create just the mock you need.

Note that if any of these methods throws an error, we want the error to bubble up to the calling code, so that it can handle the error as it sees fit. If it's being called by the API code, it may translate the error into an appropriate HTTP response code. If it's being called by a web interface, it may translate the error into an appropriate message to be displayed to the user, and so on. The point is this function doesn't know how to handle the errors that may be thrown.

For an example, consider someone adding a call to oven.preheat() (optimization!) in your cookie baking example:

deleted 103 characters in body
Source Link
hoffmale
  • 777
  • 5
  • 10

(Additional thought exercise: should your unit test break if someone adds a line oven.preheat()?)

You should be able to deduce all the required data for these tests from the dough, pan and oven objects themselves. No need to capture the function calls. Treat the function as if its implementation would not be available to you!

(Additional thought exercise: should your unit test break if someone adds a line oven.preheat()?)

You should be able to deduce all the required data for these tests from the dough, pan and oven objects themselves. No need to capture the function calls. Treat the function as if its implementation would not be available to you!

You should be able to deduce all the required data for these tests from the dough, pan and oven objects themselves. No need to capture the function calls. Treat the function as if its implementation would not be available to you!

added 4837 characters in body
Source Link
hoffmale
  • 777
  • 5
  • 10

For your latest addition:

When a user creates a new account, a number of things need to happen: 1) a new user record needs to be created in the database 2) a welcome email needs to be sent 3) the user's IP address needs to be recorded for fraud purposes.

So we want to create a method that ties together all the "new user" steps:

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

For a function like this i would mock/stub/fake (whatever seems more general) the dataStore and emailService parameters. This function does not do any state transitions on any parameter on its own, it delegates them to methods of some of them. I would try to verify that the call to the function did 4 things:

  • it inserted a user into the data store
  • it sent (or at least called the corresponding method) a welcome email
  • it recorded the users IP into the data store
  • it delegated any exception/error it encountered (if any)

The first 3 checks can be done with mocks, stubs or fakes of dataStore and emailService (you really don't want to send emails when testing). Since I had to look this up for some of the comments, these are the differences:

  • A fake is an object that behaves the same things as the original and is to a certain extent indistinguishable. Its code can normally be reused across tests. This can, for example, be a simple in-memory database for a database wrapper.
  • A stub just implements as much as needed to fulfill the required operations of this test. In most cases, a stub is specific to a test or a group of tests requiring only a small set of the methods of the original. In this example, it could be a dataStore that just implements a suitable version of insertUserRecord() and recordIpAddress().
  • A mock is an object that lets you verify how it is used (most often by letting you evaluate calls to its methods). I'd try to use them sparely in unit tests since you actually try to test the function implementation and not the adherence to its interface, but they still have their uses. Many mock frameworks exists to help you create just the mock you need.

Note that if any of these methods throws an error, we want the error to bubble up to the calling code, so that it can handle the error as it sees fit. If it's being called by the API code, it may translate the error into an appropriate http response code. If it's being called by a web interface, it may translate the error into an appropriate message to be displayed to the user, and so on. The point is this function doesn't know how to handle the errors that may be thrown.

Expected exceptions/errors are valid test cases: You confirm, that, in case such an event happens, the function behaves the way you expect it would. This can be achieved by letting the corresponding mock/fake/stub object throw when desired.

The essence of my confusion is that to unit test such a function it seems necessary to repeat the exact implementation in the test itself (by specifying that methods are called on mocks in a certain order) and that seems wrong.

Sometimes this has to be done (though you mostly care about this in integration tests). More often, there are other ways to verify the expected side effects/state changes.

Verifying exact functions calls makes for rather brittle unit tests: Only small changes to the original function causes them to fail. This can be desired or not, but it requires a change to the corresponding unit test(s) whenever you change a function (be it refactoring, optimizing, bug fixing, ...).

Sadly, in that case the unit test loses some of its credibility: since it was changed, it does not confirm the function after the change behaves the same way as before.

For an example, consider someone adding a call to oven.preheat() (optimization!) in you cookie baking example:

  • If you mocked the oven object, it won't expect that call and fail the test, although the observable behavior of the method did not change (you still have a pan of cookies, hopefully).
  • A stub might or might not fail, depending on whether you only added the methods to be tested or the whole interface with some dummy methods.
  • A fake should not fail, since it should implement the method (according to the interface)

In my unit tests, I try to be as general as possible: If the implementation changes, but the visible behavior (from the perspective of the caller) is still the same, my tests should pass. Ideally, the only case I need to change an existing unit test should be a bug fix (of the test, not the function under test).


For your latest addition:

When a user creates a new account, a number of things need to happen: 1) a new user record needs to be created in the database 2) a welcome email needs to be sent 3) the user's IP address needs to be recorded for fraud purposes.

So we want to create a method that ties together all the "new user" steps:

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

For a function like this i would mock/stub/fake (whatever seems more general) the dataStore and emailService parameters. This function does not do any state transitions on any parameter on its own, it delegates them to methods of some of them. I would try to verify that the call to the function did 4 things:

  • it inserted a user into the data store
  • it sent (or at least called the corresponding method) a welcome email
  • it recorded the users IP into the data store
  • it delegated any exception/error it encountered (if any)

The first 3 checks can be done with mocks, stubs or fakes of dataStore and emailService (you really don't want to send emails when testing). Since I had to look this up for some of the comments, these are the differences:

  • A fake is an object that behaves the same things as the original and is to a certain extent indistinguishable. Its code can normally be reused across tests. This can, for example, be a simple in-memory database for a database wrapper.
  • A stub just implements as much as needed to fulfill the required operations of this test. In most cases, a stub is specific to a test or a group of tests requiring only a small set of the methods of the original. In this example, it could be a dataStore that just implements a suitable version of insertUserRecord() and recordIpAddress().
  • A mock is an object that lets you verify how it is used (most often by letting you evaluate calls to its methods). I'd try to use them sparely in unit tests since you actually try to test the function implementation and not the adherence to its interface, but they still have their uses. Many mock frameworks exists to help you create just the mock you need.

Note that if any of these methods throws an error, we want the error to bubble up to the calling code, so that it can handle the error as it sees fit. If it's being called by the API code, it may translate the error into an appropriate http response code. If it's being called by a web interface, it may translate the error into an appropriate message to be displayed to the user, and so on. The point is this function doesn't know how to handle the errors that may be thrown.

Expected exceptions/errors are valid test cases: You confirm, that, in case such an event happens, the function behaves the way you expect it would. This can be achieved by letting the corresponding mock/fake/stub object throw when desired.

The essence of my confusion is that to unit test such a function it seems necessary to repeat the exact implementation in the test itself (by specifying that methods are called on mocks in a certain order) and that seems wrong.

Sometimes this has to be done (though you mostly care about this in integration tests). More often, there are other ways to verify the expected side effects/state changes.

Verifying exact functions calls makes for rather brittle unit tests: Only small changes to the original function causes them to fail. This can be desired or not, but it requires a change to the corresponding unit test(s) whenever you change a function (be it refactoring, optimizing, bug fixing, ...).

Sadly, in that case the unit test loses some of its credibility: since it was changed, it does not confirm the function after the change behaves the same way as before.

For an example, consider someone adding a call to oven.preheat() (optimization!) in you cookie baking example:

  • If you mocked the oven object, it won't expect that call and fail the test, although the observable behavior of the method did not change (you still have a pan of cookies, hopefully).
  • A stub might or might not fail, depending on whether you only added the methods to be tested or the whole interface with some dummy methods.
  • A fake should not fail, since it should implement the method (according to the interface)

In my unit tests, I try to be as general as possible: If the implementation changes, but the visible behavior (from the perspective of the caller) is still the same, my tests should pass. Ideally, the only case I need to change an existing unit test should be a bug fix (of the test, not the function under test).

added some clarification about the mocking of the data store, changed some expressions
Source Link
hoffmale
  • 777
  • 5
  • 10
Loading
added a bit of formatting, clarified a sentence
Source Link
hoffmale
  • 777
  • 5
  • 10
Loading
Source Link
hoffmale
  • 777
  • 5
  • 10
Loading