This is the canonical article where many differences in doing TDD are explained.

Verification differences

It starts with a difference in verification. You may make assertions about the state of a System Under Test (SUT) and its collaborators:

protected void setUp() throws Exception {
    warehouse.add(TALISKER, 50);
	// ...
}
public void testOrderDoesNotRemoveIfNotEnough() {
    Order order = new Order(TALISKER, 51);
    order.fill(warehouse);
    assertFalse(order.isFilled());
    assertEquals(50, warehouse.getInventory(TALISKER));     // <--
}

Or you may make assertions about behavior, whether we make the right calls to functions:

 public void testFillingDoesNotRemoveIfNotEnoughInStock() {
    Order order = new Order(TALISKER, 51);    
    Mock warehouse = mock(Warehouse.class);
      
    warehouse.expects(once()).method("hasInventory")        // <--
      .withAnyArguments()
      .will(returnValue(false));
 
    order.fill((Warehouse) warehouse.proxy());
 
    assertFalse(order.isFilled());
}

Test doubles differences

Gerard Meszaros defines a Test double as a generic term for any object used in place of a real object in testing. Tests double can be among other things:

  • Stubs: fake objects who provide canned answers to calls made during the test.
  • Mocks: we use expectations of the calls they are going to receive.

Now this gets a bit confusing: a mock is intended to be used for behavior verification. Stubs are usually used for state verification, but it can do both. However, people often use mock frameworks to build stubs in an easier way.

TDD approach differences

Based on the above differences, we have:

A mockist TDD is usually outside-in or top-down:

Once you have your first test running, the expectations on the mocks provide a specification for the next step and a starting point for the tests. You turn each expectation into a test on a collaborator and repeat the process working your way into the system one SUT at a time. This style is also referred to as outside-in, which is a very descriptive name for it. It works well with layered systems. You first start by programming the UI using mock layers underneath. Then you write tests for the lower layer, gradually stepping through the system one layer at a time. This is a very structured and controlled approach, one that many people believe is helpful to guide newcomers to OO and TDD.

A classical TDD can do that, with stubs first and later with real objects:

You can do a similar stepping approach, using stubbed methods instead of mocks. To do this, whenever you need something from a collaborator you just hard-code exactly the response the test requires to make the SUT work. Then once you’re green with that you replace the hard coded response with a proper code.

but usually a classical TDD will be middle-out or bottom-up:

In this style you take a feature and decide what you need in the domain for this feature to work. You get the domain objects to do what you need and once they are working you layer the UI on top. Doing this you might never need to fake anything. A lot of people like this because it focuses attention on the domain model first, which helps keep domain logic from leaking into the UI.

Cons of classical TDD

If you introduce a bug to a system with mockist testing, it will usually cause only tests whose SUT contains the bug to fail. With the classic approach, however, any tests of client objects can also fail, which leads to failures where the buggy object is used as a collaborator in another object’s test. As a result a failure in a highly used object causes a ripple of failing tests all across the system.

But if you are running your unit tests frequently, you will usually know which change introduced these errors.

Cons of mockist TDD

In essence classic xunit tests are not just unit tests, but also mini-integration tests. As a result many people like the fact that client tests may catch errors that the main tests for an object may have missed, particularly probing areas where classes interact. Mockist tests lose that quality. In addition you also run the risk that expectations on mockist tests can be incorrect, resulting in unit tests that run green but mask inherent errors.

This can be balanced with having integration tests.

When you write a mockist test, you are testing the outbound calls of the SUT to ensure it talks properly to its suppliers. A classic test only cares about the final state - not how that state was derived. Mockist tests are thus more coupled to the implementation of a method. Changing the nature of calls to collaborators usually cause a mockist test to break.

Other notes

 I first came across the term “mock object” a few years ago in the Extreme Programming (XP) (…) mock objects came out of the XP community.