Growing Object-Oriented Software

Guided by Tests

Wed 8th January 2020

Table of Contents

  1. 1. What Is the Point of Test-Driven Development?
    1. The Bigger Picture
    2. Levels of Testing
    3. External and Internal Quality
    4. Coupling and Cohesion
  2. 2. Test-Driven Development with Objects
    1. Tell, Don’t Ask
    2. Support for TDD with Mock Objects
  3. 3. An Introduction to the Tools
  4. 4. Kick-Starting the Test-Driven Cycle
  5. 5. Maintaining the Test-Driven Cycle
  6. 6. Object Oriented Style
  7. 7. Achieving Object-Oriented Design
    1. How Writing a Test First Helps the Design
    2. Communication over Classification
    3. Value Types
    4. Object Types
    5. Identify relationships with interfaces
    6. Refactor interfaces too
    7. Building up to higher-level programming
    8. And what about classes?

1. What Is the Point of Test-Driven Development?

The loop is:

Writing tests first:

The golden rule of TDD:

Never write new functionality without a failing test.

The Bigger Picture

For developing features, we have a similar loop, but with acceptance tests instead of unit tests. This should ideally be end-to-end, interacting from the system from the outside.

Levels of Testing

External and Internal Quality

External Quality is how well a system meets the needs of its users - does it do the right thing, is it fast, is it reliable. Internal Quality is how well a system meets the needs of its developers - is it easy to understand, is it easy to work with.

Acceptance testing speaks to external quality. Unit testing speaks to internal quality. Integration testing is somewhere in the middle.

Coupling and Cohesion

These are two measures of how hard it will be to change some code. Elements are tightly coupled if a change in one necessitates a change in another. An element is cohesive if it has clear and well defined responsibilities.

2. Test-Driven Development with Objects

OOP focuses on the communication between objects.

It’s important to distinguish between Values and Objects. Values are immutable instances that model fixed quantities - they are equal if their contents are equal. Objects can change and have an identity.

In Java, object Roles are defined by Interfaces. A Role is a set of related Responsibilities. An Object implements one or more Roles. A Collaboration is an interaction between Objects/Roles.

A simple design technique is CRC cards. Create index cards for each Candidate (potential class) and write a list of Responsibilities and Collaborators on them.

Tell, Don’t Ask

A calling object should describe what it wants in terms of the object it is calling. For example:

((EditSaveCustomizer) master.getModelisable()
    .getDockablePanel()
    .getCustomizer())
  .getSaveItem().setEnabled(Boolean.FALSE.booleanValue());

Should become:

master.allowSavingOfCustomisations();

This is the Law of Demeter. It allows you to swap objects around more easily as it doesn’t bind your class to the implementations of the class you’re calling. It also makes the interactions between objects explicit.

Support for TDD with Mock Objects

Since a component’s state is internal to it, the best way to test it is to provide Mock objects and test its interactions with these mocks.

3. An Introduction to the Tools

A test fixture is the fixed state that exists at the start of a test.

4. Kick-Starting the Test-Driven Cycle

A walking skeleton is an implementation of the smallest amount of real functionality that can automatically build, deploy and test end-to-end. The functionality is trivially simple so we can concentrate on infrastructure. Eg. for a database backed web app, it would show a flat web page with info from the database.

We want our test to start from scratch, build a deployable system, deploy it and run the tests. Creating this will make us think somewhat of the overall structure (but not design everything at the outset).

Creating the walking skeleton will take a fair amount of time - it will include a bunch of set up and infrastructure choices.

5. Maintaining the Test-Driven Cycle

Start writing each feature as a failing acceptance test. This should be written in terms of the application’s domain, not knowing about any of the underlying technologies.

Start testing with the simplest success case. Keep a notepad to jot down failure cases/refactorings and other technical tasks as you program.

When writing the failing test, concentrate on making it easy to read and understand. When it reads well, you can then concentrate on infrastructure to support the test.

Run the test first, to make sure it fails and does so in the expected way.

You should unit-test behaviours/features, not methods.

If a unit/integration test is hard to write, pay attention to why and how the design can change to make it easier.

6. Object Oriented Style

Separation of concerns - when we change the behaviour of a system, we should only change as little code as possible. Also think about moving to Higher levels of abstraction.

Cockburn’s ports and adapters keeps code for business is isolated from the technical infrastructure:

It’s used in hexagonal architecture. The difference between hexagonal architecture and layered architecture is that if two technologies need to talk to each other, they do so through the business layer. Business logic is described in terms of what business people understand.

It can be a pain mapping between the domain models of objects (eg coping the data from the business User class to the database User class), but there are tools to help with this. If you’re in a different domain you probably shouldn’t be using the same objects. Eg. ProtoBuffers only be used for communication, not as a domain object.

Encapsulation and Information Hiding:

You can break encapsulation by sharing references to mutable objects (aliasing). You should define immutable value types, avoid global variables, perform defensive copies.

An object’s Peers can roughly be split into:

Dependencies should be passed into the constructor, notifications and adjustments can be set/added.

Objects should be context independent - they should not have built in knowledge about the surrounding system.

Keep a record of architecture decisions.

7. Achieving Object-Oriented Design

How Writing a Test First Helps the Design

Starting with a test gets us to describe what we want to achieve, not how. Keeping unit tests clear keeps us at the right level of abstraction and information hiding. Keeping them short means that we limit the scope of the object.

To construct the object for testing, we’ve got to pass in its dependencies. By considering how you’d make the object in an environment other than its main one, you help with context independence.

Communication over Classification

As mentioned before, how objects communicate is more important than class structure.

An interface dictates if two objects will fit together, a protocol says if they will work together. By using mocks (eg, checking that methods of the dependencies are called), we make this visible.

Value Types

Values are immutable (and so don’t have an identity), objects have state, so they have identity and therefore relationships. Brief shout out to Domain Driven Design - you should use Value types for concepts in the domain model.

This is how Value types come about through refactoring:

Object Types

Similar to the above:

Identify relationships with interfaces

Interfaces should be as narrow as possible (this tends to happen with the Budding Off approach above).

Having an interface named Thing and an implementation named ThingImpl is probably a sign of bad design. The interface should be described in general, domain language and the implementation should have something specific about it to use in the name.

Refactor interfaces too

As you gather interfaces, consider whether similar interfaces are really the same and if so combine them. If not, make them more different.

Building up to higher-level programming

Split code into an implementation layer and a declarative layer. The implementation layer is the graph of objects. The declarative layer builds up this graph and you can make a bunch of convenience functions to make that nicer - it becomes a DSL.

And what about classes?

We don’t consider class hierarchies in the design of the system - they arise out of refactoring common code (although delegation is usually better).