|
Overview
This article describes difficulties waiting for us when we make
code driven by automated tests. Reader will
go through a process of implementation of a miniature system
performing some algebraic calculations with vectors.
We'll use TDD (Test Driven Development) approach to demonstrate
"test first" way of thinking. The cornerstone of our
discussion is a technique of method mocking which forces automated
test be really modular.
Good test vs Bad test
A test is robust, clear and useful when it shows how code should work
but not how it does. It is important because we have to consider tests
as business rules, active specification or up to date documentation but
not as executable appendage breaking each time when we change a code.
Thus, the main our goal is to make JUnit tests to be really modular.
If you are not familiar with JUnit or JMock, I recommend to read the
article about automated testing in Java
before. It is detailed introduction to the topic for beginners. Otherwise,
I believe that you know what we use JMock for and why developers write
automated tests at all.
OK. We've found that test should be oriented on contract rather then
implementation. It is a good point. But what the contract is and how
to make class execute its contract. Everyone knows that class' contract
is described by its interface. Concrete implementation satisfies conditions
listed in contract only on the assumption of proper work of classes it
depends on. And again, these classes should provide well defined interfaces.
That's why it is always better to supply almost each your class with interface
if you want to test a code essentially. Moreover each class already have
an interface implicitly. It is described by its public methods.
Needless to say, there are two kinds of modules in Object Oriented Programming:
classes itself and class methods. Just like as interconnection among classes
is determined by their interfaces, interconnection among methods is based on
method contracts. It includes parameters, returned value, thrown exceptions
and rules of data processing.
Summarizing it with requirement of test robustness, we come to conclusion
that the most useful and really modular tests are those tests which cover
behaviour of a single method in the assumption that all other methods work
properly. Thus, we have to mock methods of a class.
Allow me to state the last idea regarding perfect test before we start coding.
As we mentioned, automated test is a piece of documentation. Thus, it is highly
recommended to cover each special case separately to make clearer active
specification. Don't worry to create a lot of tests, there is nothing bad
excepting high time costs. I suggest to cover all edge cases as well to catch
possible bugs as early as it possible.
|
|
TDD in action
At least, we can start. Our system doesn't pretend to be the best
implementation of vector space. But it is simple enough to exercise on.
First and simplest its component is a Vector.
Remember that we are using TDD. What we have to do at first is
understand an idea of Vector and create a list of tests covering its contract.
Artefacts:
- Vector has two coordinates: X and Y
- Vector has a length
As you can see, it is simple. Only one note here. Customer asked us
to create a system in 2D. Therefore, we should not care for Z coordinate
in this step because Agile forces us to provide the most valuable
features with maximal speed. By the way, it is a very hard task
to find a balance between
flexibility/quality and speed/cost.
Tests:
- A lenght of vector (0.0, 0.0) is 0.0
- A length of vector (1.0, 1.0) is sqrt(2.0)
- A length of vector (0.0, 1.0) = length of vector (1.0, 0.0) = 1.0
- A length of vector (-1.0, -1.0) = sqrt(2.0)
|
|
It is simple as well. Now we understand how vector should work
so that we can start making a test:
package com.intelrate.vector.impl;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class VectorImplTest
{
private final double delta = 0.000001;
@Test
public void testLength1()
{
final Vector vector = new VectorImpl(0.0, 0.0);
assertEquals(0.0, vector.length(), delta);
}
}
Looks fine but doesn't work. It will not even compile bacause we haven't
yet defined neither Vector interface nor VectorImpl class. Thus, our next
step is to make this test pass. We need Vector interface.
package com.intelrate.vector;
public interface Vector
{
double length();
}
And its implementation VectorImpl:
public class VectorImpl implements Vector
{
private double x;
private double y;
public VectorImpl(final double x, final double y)
{
this.x = x;
this.y = y;
}
@Override
public double length()
{
return 0.0;
}
}
Run VectorImplTest to see green line and enjoy. But why did I write wrong implementation
of the length() method? It is not wrong because test is green. Don't be in a hurry when
you program. The most common way to make a mistake is to write wrong code before test and
then forget about test at all.
As opposed to writting all "correct" code at once we should move forward step by step.
Now we see that not all tests from our list exist. The next test is:
@Test
public void testLength2()
{
final Vector vector = new VectorImpl(1.0, 1.0);
assertEquals(Math.sqrt(2.0), vector.length(), delta);
}
Run this test to check that your implementation of class Vector fits well:
Wow! We've made a mistake. Return to code and fix it:
@Override
public double length()
{
return Math.sqrt(x * x + x * x);
}
Run tests again:
Before you hit me hard for this wrong code I want you to understand that
it is pretty cool when you see green line. Everyone likes green line.
You may think that I had grade D or C at school but there were no such grades
where I studied. The idea concludes in absence of perfect developers always
making bug-free code. If our system was complex we would make a lot of
such errors. Test driven development secures our system in case we write
rational and sufficient tests. For instance:
@Test
public void testLength3()
{
final Vector vector1 = new VectorImpl(1.0, 0.0);
final Vector vector2 = new VectorImpl(0.0, 1.0);
assertEquals(1.0, vector1.length(), delta);
assertEquals(vector1.length(), vector2.length(), delta);
}
Run it as usual too discover my fault:
As you can see, it says that code is trash. Here is the fix:
@Override
public double length()
{
return Math.sqrt(x * x + y * y);
}
Don't forget to perform two actions: 1) Run all tests 2) Enjoy green line.
At the moment you may imagine that your code is correct and perfect.
Even if it is a true, you have to follow the test plan:
@Test
public void testLength2()
{
final Vector vector = new VectorImpl(-1.0, -1.0);
assertEquals(Math.sqrt(2.0), vector.length(), delta);
}
Run it:
You should be proud of your code! All tests have been implemented and line is green.
But remember that Vector should have X and Y coordinates. It is
required to supply interface with two getters. Usually getters are
too easy to broke so there is no need to cover it with JUnit. It is
a final version of Vector:
package com.intelrate.vector;
public interface Vector
{
double length();
double getX();
double getY();
}
And its implementation:
public class VectorImpl implements Vector
{
private double x;
private double y;
public VectorImpl(final double x, final double y)
{
this.x = x;
this.y = y;
}
@Override
public double length()
{
return Math.sqrt(x * x + y * y);
}
@Override
public double getX()
{
return x;
}
@Override
public double getY()
{
return y;
}
}
|
|
Vector Space
Hopefully we've just finished with baby code and now can move
toward more complex part of our system - Vector Space.
I've just shown that it is important to write a program step by step.
TDD prevents your from possible mistakes if you don't make haste and
your steps are really small. Of course, you should always think
what code you write but if tests are really good then your are done
when line is green. When developer obtains more experience in TDD their
steps are getting bigger. But one rule should be conformed: write test
first. There is a relative freedom - you can write test before code or
code after test. Both approaches are welcome so life seems to be easy.
Analysing Vector Space we've come to the next artefacts:
- Scalar product on two vectors should be determined in Vector Space
- An angle between two vectors should be determined in Vector Space
Now we should create a list of tests documenting requirements to the system.
Usually a list is being extended during implementation but I'll allow
myself to enumerate all tests we need right here:
- Scalar product of (0.0, 1.0) and (1.0, 0.0) is 0.0
- Scalar product of (2.0, 1.0) and (1.0, 2.0) is 4.0
- Scalar product of (3.0, -1.0) and (1.0, 1.0) is 2.0
- An angle between (0.0, 1.0) and (1.0, 0.0) is equal to 90°
- An angle between (√3.0, 0.0) and (√3.0, 1.0) is equal to 30°
|
|
During a work on the first test we should not care for implementation of Vector Space but
we should understand what is required to calculate scalar product. It is always better
to create mock objects because in that case we can control that Vector Space really uses
everything that it must use. Therefore, we mock two vectors and expect that getters
will be called:
package com.intelrate.vector.impl;
import static org.junit.Assert.assertEquals;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.junit.Test;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.runner.RunWith;
@RunWith(JMock.class)
public class VectorSpaceImplTest
{
private final double delta = 0.000001;
private Mockery context = new JUnit4Mockery();
private VectorSpace vectorSpace = new VectorSpaceImpl();
private Vector vector1 = context.mock(Vector.class, "vector1");
private Vector vector2 = context.mock(Vector.class, "vector2");
@Test
public void testScalarProduct1()
{
final double x1 = 0.0, x2 = 1.0, y1 = 1.0, y2 = 0.0;
final double expectedScalarProduct = 0.0;
context.checking(new Expectations() { {
one(vector1).getX(); will(returnValue(x1));
one(vector2).getX(); will(returnValue(x2));
one(vector1).getY(); will(returnValue(y1));
one(vector2).getY(); will(returnValue(y2));
} });
assertEquals(expectedScalarProduct,
vectorSpace.scalarProduct(vector1, vector2), delta);
}
@Test
public void testScalarProduct2()
{
final double x1 = 2.0, x2 = 1.0, y1 = 1.0, y2 = 2.0;
final double expectedScalarProduct = 4.0;
context.checking(new Expectations() { {
one(vector1).getX(); will(returnValue(x1));
one(vector2).getX(); will(returnValue(x2));
one(vector1).getY(); will(returnValue(y1));
one(vector2).getY(); will(returnValue(y2));
} });
assertEquals(expectedScalarProduct,
vectorSpace.scalarProduct(vector1, vector2), delta);
}
@Test
public void testScalarProduct3()
{
final double x1 = 3.0, x2 = 1.0, y1 = -1.0, y2 = 1.0;
final double expectedScalarProduct = 2.0;
context.checking(new Expectations() { {
one(vector1).getX(); will(returnValue(x1));
one(vector2).getX(); will(returnValue(x2));
one(vector1).getY(); will(returnValue(y1));
one(vector2).getY(); will(returnValue(y2));
} });
assertEquals(expectedScalarProduct,
vectorSpace.scalarProduct(vector1, vector2), delta);
}
}
Please note that you have to give explicit names to mock vectors.
Otherwise, JMock gives it default name based on class name so
IllegalArgumentException will be thrown with message "a mock with
name vector already exists". And never forget delta when compare
values of type double. You should not check manually that all expectations
are satisfied because JMock does it for you implicitly.
To make the test above compile we must provide VectorSpace interface:
package com.intelrate.vector;
public interface VectorSpace
{
/**
* Calculates a scalar product of two given vectors.
*/
double scalarProduct(final Vector vector1, final Vector vector2);
}
And its implementation VectorSpaceImpl:
package com.intelrate.vector.impl;
import com.intelrate.vector.Vector;
import com.intelrate.vector.VectorSpace;
public class VectorSpaceImpl implements VectorSpace
{
@Override
public double scalarProduct(final Vector vector1, final Vector vector2)
{
return vector1.getX() * vector2.getX() + vector1.getY() * vector2.getY();
}
}
According to our technique, run all JUnit tests:
It is very important to have all tests successfully passing. Once some
test is broken, repairing of it should be a highest priority task.
I strongly recommend to use Continuous Integration. Automate the build,
make it self-testing and always keep it in stable state.
So far so good. Now we are smart enough to discuss the most interesting
question of our investigation. Let us implement the last two tests from the list.
It is known that scalar product of vectors A and B is equal to |A| * |B| * cos(A^B).
Thus, getters and length methods will be called for each vector:
@Test
public void testAngle1()
{
final double x1 = 0.0, x2 = 1.0, y1 = 1.0, y2 = 0.0;
final double expectedAngle = 90.0;
context.checking(new Expectations() { {
one(vector1).getX(); will(returnValue(x1));
one(vector2).getX(); will(returnValue(x2));
one(vector1).getY(); will(returnValue(y1));
one(vector2).getY(); will(returnValue(y2));
one(vector1).length(); will(returnValue(1.0));
one(vector2).length(); will(returnValue(1.0));
} });
assertEquals(expectedAngle,
vectorSpace.angle(vector1, vector2), delta);
}
@Test
public void testAngle2()
{
final double x1 = Math.sqrt(3.0), x2 = Math.sqrt(3.0), y1 = 0.0, y2 = 1.0;
final double expectedAngle = 30.0;
context.checking(new Expectations() { {
one(vector1).getX(); will(returnValue(x1));
one(vector2).getX(); will(returnValue(x2));
one(vector1).getY(); will(returnValue(y1));
one(vector2).getY(); will(returnValue(y2));
one(vector1).length(); will(returnValue(Math.sqrt(3.0)));
one(vector2).length(); will(returnValue(2.0));
} });
assertEquals(expectedAngle,
vectorSpace.angle(vector1, vector2), delta);
}
Add method angle to the VectorSpace Interface:
package com.intelrate.vector;
public interface VectorSpace
{
/**
* Calculates a scalar product of two given vectors.
*/
double scalarProduct(final Vector vector1, final Vector vector2);
/**
* Calculates an angle between vectors.
*
* @return angle in degrees
*/
double angle(final Vector vector1, final Vector vector2);
}
And implement it:
@Override
public double angle(final Vector vector1, final Vector vector2)
{
final double scalar = scalarProduct(vector1, vector2);
final double square = Math.abs(vector1.length() * vector2.length());
final double piDegree = 180.0;
return Math.acos(scalar / square) * piDegree / Math.PI;
}
As you may consider, we'll get green line as a result of tests execution:
That's great! Actually, we'll not change VectorSpaceImpl class any more.
Nevertheless, if you work on real system using TDD, refactoring of code is the must.
I've encountered that TDD forces us to put all the code in one class and,
what is more, in one method. Neddless to explain why it is bad.
Our system is small so I managed to implement VectorSpaceImpl well at once.
Further coding concerns to improvement of VectorSpaceImplTest.
Look at this:
@RunWith(JMock.class)
public class VectorSpaceImplTest
{
private final double delta = 0.000001;
private Mockery context = new JUnit4Mockery();
private VectorSpace vectorSpace = new VectorSpaceImpl();
private Vector vector1 = context.mock(Vector.class, "vector1");
private Vector vector2 = context.mock(Vector.class, "vector2");
@Test
public void testAngle2()
{
final double x1 = Math.sqrt(3.0), x2 = Math.sqrt(3.0), y1 = 0.0, y2 = 1.0;
final double expectedAngle = 30.0;
context.checking(new Expectations() { {
one(vector1).getX(); will(returnValue(x1));
one(vector2).getX(); will(returnValue(x2));
one(vector1).getY(); will(returnValue(y1));
one(vector2).getY(); will(returnValue(y2));
one(vector1).length(); will(returnValue(Math.sqrt(3.0)));
one(vector2).length(); will(returnValue(2.0));
} });
assertEquals(expectedAngle,
vectorSpace.angle(vector1, vector2), delta);
}
}
The test is brittle and it is too hard to prepare test data.
Test testAngle2() will fail not only if the method calculating
angle is broken. If we make a mistake in calculation of
scalar product, it will affect testAngle2() as well.
Fortunately, it doesn't depend on vector.length() implementation
because we use mock vectors.
As a result of dependency on scalarProduct method, we
have to mock too many calls required for its proper work.
Imagine that you have a method invoking 5 or 7 other
methods of a class containing it. It means that you will
copy and paste expectations for all these methods as we've
done it for scalar product calculation.
At the beginning of this article we've figured out that test should be modular.
Don't afraid to refactor your tests because it is a part of your system and
evolves with it. But what can we do? The simplest solution consists in
introduction of inner class extending a class under the test:
@RunWith(JMock.class)
public class VectorSpaceImplTest
{
private final double delta = 0.000001;
private Mockery context = new JUnit4Mockery();
private Vector vector1 = context.mock(Vector.class, "vector1");
private Vector vector2 = context.mock(Vector.class, "vector2");
private boolean scalarIsInvoked;
@Test
public void testAngle2()
{
final VectorSpace vectorSpace = new VectorSpaceImpl() {
public double scalarProduct(final Vector vector1, final Vector vector2) {
scalarIsInvoked = true;
return 3.0;
}
};
final double expectedAngle = 30.0;
context.checking(new Expectations() { {
one(vector1).length(); will(returnValue(Math.sqrt(3.0)));
one(vector2).length(); will(returnValue(2.0));
} });
scalarIsInvoked = false;
assertEquals(expectedAngle,
vectorSpace.angle(vector1, vector2), delta);
assertTrue(scalarIsInvoked);
}
}
Run the test to ensure that everything is fine. This version
is robust because it doesn't depend on implementation of
scalarProduct() method. At the same time it strictly verifies
a contract of angle() excluding possibility of overlapped
mistakes producing correct result. And, in my opinion,
it is much more easier to understand by this test
how an angle() method should work.
But scalarIsInvoked variable doesn't look like a cool thing at all.
It is also better to see human readable expectations rather then
operate with private fields. The next piece of code resolves these issues:
@RunWith(JMock.class)
public class VectorSpaceImplTest
{
private final double delta = 0.000001;
private Mockery context = new JUnit4Mockery();
private Vector vector1 = context.mock(Vector.class, "vector1");
private Vector vector2 = context.mock(Vector.class, "vector2");
private VectorSpace mockVectorSpace = context.mock(VectorSpace.class);
@Test
public void testAngle2()
{
final VectorSpace vectorSpace = new VectorSpaceImpl() {
public double scalarProduct(final Vector vector1, final Vector vector2) {
return mockVectorSpace.scalarProduct(vector1, vector2);
}
};
final double expectedAngle = 30.0;
context.checking(new Expectations() { {
one(vector1).length(); will(returnValue(Math.sqrt(3.0)));
one(vector2).length(); will(returnValue(2.0));
one(mockVectorSpace).scalarProduct(vector1, vector2); will(returnValue(3.0));
} });
assertEquals(expectedAngle,
vectorSpace.angle(vector1, vector2), delta);
}
}
This code is clearer and looks more proficient. At the same time it
provides full access to all abilities of JMock including parameter
verification which was hard to implement using previous approach.
Don't forget to run all tests before you go home!
It was the last idea I wanted to talk about today. Big thanks to
Dmitry Kalashnikov who suggested how to mock class method in
the best way. Special thanks to Dmitry Lobasev for very productive
seminar about test driven development.
And thank you, my dear reader, for your attention and tolerance to
my English :)
|