Financial AgileWhere software & financial engineering meet

Triangle

Print

Financial Agile is not just a web-site, it’s a community of people, based in Rotterdam, Amsterdam and London, who care about how the industry evolves.  One thing we do is coach junior financial engineers.  For example, recently I have been teaching two fellows, Vlad and Paul, and together they are looking at the triangle problem.  They both want to learn the full range of skills they need to effectively do their jobs.  Programming; test-driven-development; object-oriented design; principled negotiation; systems thinking; etc.  Jumping from a course in C# to TDD with mocks à la Freeman and Pryce is too much.  Yet, TDD with mocks à la Freeman and Pryce is a skill that all competent financial engineers have to master.

In his 1999 book, Testing Object-Oriented Systems, Binder challenges his readers.  Binder adapted an exercise from Myers' The Art of Software Testing.  It went like this,

[define a test plan for a system] that reads three integers from a card.  The three values are interpreted as representing the lengths of the sides of a triangle.  The program prints a message that states whether the triangle is scalene, isosceles, or equilateral.
On a sheet of paper, write test cases (i.e., specific sets of data) that you feel would adequately test this program.  (Binder 1999, Myers 1976)

The triangle is an interesting starting point because it’s not too complex.  It encompasses a number of challenges, such as, test data generation, testing a void method, object equality, and it introduces the the idea of a Test Driver and an assertion library, which, in the case of this example, is JUnit.  Importantly, when you ask a student to develop the tests and the triangle code together, it becomes an exercise in evolution.  Whatever your official methodology is, in practice, problems always co-evolve with their solutions.  It is worth remembering, and it is worth internalising, Cross and Dorst,

It is widely accepted that creative design is not a matter of first fixing the problem and then searching for a satisfactory solution concept; instead it seems more to be a matter of developing and refining together both the formulation of the problem and ideas for its solution, with constant iterations of analysis, synthesis and evaluation processes between the two “spaces” - problem and solution.  (Cross & Dorst, 1999.)

The triangle problem, then, is a bounded exercise that, when looked at properly, is a rich training ground for financial or software engineers.  In this short note, which we can think of as part 1 in this series, I am going to look at the bare minimum of test cases for the three simplest triangle methods.  The reader should stop now and consider the small part of the triangle’s interface that I will look at today.  The reader should also try and come up with the test cases before reading on.

public class Triangle {
 public Triangle(int a, int b, int c);
 public boolean isIsosceles();
 public boolean isEquilateral();
 public boolean isScalene();
}


Simple Cases
I refactor duplicate code as I write my tests.  A pattern emerges.  I have one public test method and one private assertion method, as shown here with the the initial three test cases.

@Test public void isEquilateral() {
 assertIsEquilateral(true, 1, 1, 1);
 assertIsEquilateral(true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
 assertIsEquilateral(false, 100, 100, 92);
}

@Test public void isIsosceles() {   
 assertIsIsosceles(true, 100, 92, 100);
 assertIsIsosceles(true, Integer.MAX_VALUE, 1500000000, Integer.MAX_VALUE);
 assertIsIsosceles(false, 100, 100, 100);
}

@Test public void isScalene() {
 assertIsScalene(true, 1500, 1000, 980);
 assertIsScalene(false, 1, 1, 1);
}

private void assertIsEquilateral(boolean expected, int a, int b, int c) {
 Triangle triangle = new Triangle(a, b, c);
 assertEquals(expected,triangle.isEquilateral());    
}

private void assertIsIsosceles(boolean expected, int a, int b, int c) {
 Triangle triangle = new Triangle(a, b, c);
 assertEquals(expected,triangle.isIsosceles());    
}

private void assertIsScalene(boolean expected, int a, int b, int c) {
 Triangle triangle = new Triangle(a, b, c);
 assertEquals(expected, triangle.isScalene());        
}


So far, I have eight test cases covering the basics.  Now I have to mix the test data up, looking at different permutations.  For example, the data set of 1500, 1000 and 980, that is used to test the scalene triangle, is valid in six different arrangements.

1500  1000  980
1500  980  1000
1000  1500  980
1000  980  1500
980  1000  1500
980  1500  1000

For an isosceles, there are three arrangements.

92, 100, 100
100, 92, 100
100, 100, 92

@Test public void isScalene() {
assertIsScalene(true, 1500, 1000, 980);
assertIsScalene(true, 1500, 980, 1000);
assertIsScalene(true, 1000, 1500, 980);
assertIsScalene(true, 1000, 980, 1500);
assertIsScalene(true, 980, 1000, 1500);
assertIsScalene(true, 980, 1500, 1000);
assertIsScalene(false, 1, 1, 1);
}

@Test public void isIsosceles() {· ·
assertIsIsosceles(true, 100, 92, 100);
assertIsIsosceles(true, 100, 100, 92);
assertIsIsosceles(true, 92, 100, 100);
assertIsIsosceles(true, Integer.MAX_VALUE, 1500000000, Integer.MAX_VALUE);
assertIsIsosceles(false, 100, 100, 100);
}

Bad Data
As well as valid data, the triangle under test has to also deal with bad data.  This bad data comes in two forms.  Firstly, anything invalid, such as a negative or null value.  Secondly, bad data that would create an incorrect triangle, such as 10, 1, 19 - which would make a strange arrangement, sketch it out if you don’t believe me.  A triangle is officially a triangle if half the sum of the sides is greater than each side.

So,  s = {a+b+c} /{2}

The following has to be true:

s>a && s>b && s>c

With the above invalid data we have:

s = {10 + 1 + 19} / {2}

15 > 10 && 15>1 && 15>19 == false.

And with some some valid data:

s = {1 + 10 + 10}/2

10.5 > 10 && 10.5 > 10 && 10.5 > 1 == true

Therefore, testing invalid data has to include garbage - e.g. -10 - and data for invalid triangles.  I want to split these up into two tests.

@Test public void garbage() {
 assertBadInput(Integer.MAX_VALUE + 1, Integer.MIN_VALUE - 1, Integer.MIN_VALUE -1);
 assertBadInput(Integer.MAX_VALUE + 1, 8, Integer.MIN_VALUE -1);
 assertBadInput(-1, -2, -3);
 
}
 
@Test public void invalidTriangles() {
 assertBadInput(10, 19, 1);
 assertBadInput(100, 1900, 1);
 assertBadInput(1000, 1, 1);
}

I decide to add as many test cases as possible, to make sure that I cannot instantiate an invalid triangle.  Once I start to do this, I realise that I am going to have a large number of assertions in the test cases.  This is bad for two reasons.  Firstly, it makes the tests less concise than they need to be.  For one case, 10, 19, 1, the test is six lines long.

@Test public void invalidTriangles() {
 assertBadInput(10, 19, 1);
 assertBadInput(10, 1, 19);
 assertBadInput(19, 1, 10);
 assertBadInput(19, 10, 1);
 assertBadInput(1, 10, 19);
 assertBadInput(1, 19, 10);
}

It would be more concise to say something like this.

@Test public void invalidTriangles() {
 assertAllPermutationsAreInvalid(10, 19, 1);
}


Secondly, typing in lots of test data is error prone.  One reason to automate tests is that you should never send a man to do a machine’s job (nor should you send a machine to do a man’s job, but the benefits of manual testing is something we’ll come back to later).  At this point, it becomes obvious that I need a simple system for generating test data.  This will benefit me in the short term, as I try to test the triangle’s simplest methods, but I am sure it will benefit me in the long term, too, as I move forward with more rigorous testing.

Side Track - Test Data Generator
Using test-driven-development, I experiment for an hour, developing both the problem specification and solution, before settling on two new classes for generating and storing test data.  The first thing I do is test that for a given data set the generator can instantiate the correct triangles.
@Test public void scalenePermutations() {                
expected.add(new Triangle(1500, 1000, 980));
 expected.add(new Triangle(1500, 980, 1000));
 expected.add(new Triangle(1000, 1500, 980));
 expected.add(new Triangle(1000, 980, 1500));
 expected.add(new Triangle(980, 1000, 1500));
 expected.add(new Triangle(980, 1500, 1000));
 assertAllTrianglesCreated(expected, 1500, 1000, 980);
}              
private void assertAllTrianglesCreated(Set                                             expected, int a, int b, int c) {
TriangleGenerator t = new TriangleGenerator(a, b, c);
 for (int i = 0; i < expected.size(); i++) {
 assertTrue(expected.contains(t.nextTriangle()));
 }
 try {
 t.nextTriangle();
 fail("There should be no more triangles.");
 } catch (NoSuchElementException e) {
 // Expected;
 }
}


The TriangleTest is now much neater.  For example, the scalene test is very concise.
@Test public void isScalene() {
 assertIsScalene(1500, 1000, 980);
 assertFalse(new Triangle(1, 1, 1).isScalene());
}

private void assertIsScalene(int a, int b, int c) {
 TriangleGenerator generator = new TriangleGenerator(a, b, c);
 while(generator.hasNextTriangle()) {
 assertTrue(generator.nextTriangle().isScalene());
 }
}


Sometimes, for example when I need permutations of bad data, I want the generator to return a data set.    
@Test public void dataSetPermutations() {
 Set expected = new HashSet();
 expected.add(new TriangleDataSet(1500, -9, 980));
 expected.add(new TriangleDataSet(1500, 980, -9));
 expected.add(new TriangleDataSet(-9, 1500, 980));
 expected.add(new TriangleDataSet(-9, 980, 1500));
 expected.add(new TriangleDataSet(980, -9, 1500));
 expected.add(new TriangleDataSet(980, 1500, -9));
 assertAllDataSetsCreated(expected, 1500, -9, 980);
}


Recap
In order to test my triangle, I wrote five tests, one for each type of triangle - isosceles, scalene, equilateral - one for garbage and one for data that would construct an invalid triangle.  In total, there are 78 test cases.  How many did you come up with?  To save error prone typing and cluttered code, I wrote a simple test data generator for different permutations of values.  In order to successfully test, I implemented the two equality methods - hashCode and equals - on the Triangle class.  For reporting purposes, I overrode the toString method.

The reader has had a brief introduction into object-oriented testing.

  • Without object equality, there can be no object oriented testing.
  • Assertions are simple statements that throw an exception if a boolean expression evaluates to false. 
  • In this example, assertions appear in the the tests - they are essential for testing - and in the triangle’s constructor.
  • Test data generators are one tool that can be used to support unit testing. 
  • The test to implementation code ratio in this example is about 2:7.  There are five classes in total, four of them are used for testing.  This ratio is, in my experience, about right for none-critical code 
  • It is the financial engineer’s job to produce working code and tests and to provide a framework that will allow the growth of their classes and systems.


Next Steps
There are a number of things to consider, for some readers, there may be more questions than answers.  Firstly, I left one test unimplemented.  How does one go about testing a void method without altering the interface of the object under test?  Secondly, why are there two classes - Triangle and TriangleTestData - that look the same?  Is this is design fault?  Thirdly, is this level of unit testing really necessary?  Surely, someone else will test the code, at a later date, right?

It’s important to reflect on the contents of this article.  Formulating questions can assist in that process.  And, of course, the comments section below will allow readers to ask questions, which we will always be happy to answer.

The next note will be about how one can test the void render method.

References/Further Reading
Binder, R. (1999) Testing Object-Oriented Systems, Addison-Wesley.
Brooks, F. P. (2010)  The Design of Design, Addison-Wesley.
Cross and Dorst, 1999, ‘Co-Evolution of Problem and Solution Spaces in Creative Design’.
Freeman, S., & Pryce, N. (2009) Growing Object -Oriented Systems, Guided by Tests, Addison-Wesley.
Myers, G. J. ((2004) [1976]) The Art of Software Testing.  Wiley.
Venners, B. (2004) 'Contract Driven Development, A Conversation with Bertrand Meyer, Part III', Artima.  Available at: http://www.artima.com/intv/contest.html.

Attachments:
Download this file (TriangleArchive.zip)Triangle Java Code[The code to accompany the traingle example]4756 Kb26/08/10 13:36

Add comment


Security code
Refresh

FUVIAZRFEB09F