Software Testing: Tutorial 1 Solutions

Here are some example solutions to the activities from Tutorial 1.

  1. Add tests: Here's a new version of TriangleTestJ4.java with more tests added. Notice that the answer sheet from the Lecture 1 quiz suggested trying all permutations of the test inputs (so don't just test that (4, 5, 6) is scalene, test also that (4, 6, 5), (5, 4, 6), (5, 6, 4), (6, 4, 5) and (6, 5, 4) are scalene), so I've implemented a new method called permutedScaleneTest() which tests that all six permutations give the right answer. I also implemented a method called scaleneTest() which runs a single test for a triangle, but prepends that triangle's details to the message that's printed if the test fails:

    TriangleTestJ4.java: testing lots of Triangles:
    package st;
    
    import static org.junit.Assert.*;
    
    import org.junit.Test;
    
    public class TriangleTestJ4 {
    
      @Test
      public void testTriangle() {
        fail("Not yet implemented");
      }
    
      // This constructs a triangle and makes the appropriate assertion
      // (either it is scalene, or it isn't scalene).  It's used by
      // permutedScaleneTest in order to test all permutations of a
      // particular triangle together.  It prefixes the triangle's dimensions
      // to the error message m.
      private void scaleneTest(int p, int q, int r, boolean scalene, String m) {
        Triangle t = new Triangle(p, q, r);
        m = "(" + p + ", " + q + ", " + r + ") " + m;
        assertEquals(m, t.isScalene(), scalene);
      }
    
      // This tests that all permutations of a triangle give the right answer
      // to isScalene().  m is used as an error message if a test fails.
      private void permutedScaleneTest(int p, int q, int r, boolean scalene, String m) {
        scaleneTest(p, q, r, scalene, m);
        scaleneTest(p, r, q, scalene, m);
        scaleneTest(q, p, r, scalene, m);
        scaleneTest(q, r, p, scalene, m);
        scaleneTest(r, p, q, scalene, m);
        scaleneTest(r, q, p, scalene, m);
      }
    
      // Most of these tests are inspired by the Tutorial 1 quiz answer sheet.
      @Test
      public void testIsScalene() {
        permutedScaleneTest(4, 5, 6, true, "should be scalene!");
        permutedScaleneTest(4, 3, 2, true, "should be scalene!");
        // No permutations needed for this one...
        scaleneTest(9, 9, 9, false, "shouldn't be scalene!");
        permutedScaleneTest(4, 4, 2, false, "shouldn't be scalene!");
        permutedScaleneTest(0, 4, 2, false, "shouldn't be scalene!");
        permutedScaleneTest(0, 0, 2, false, "shouldn't be scalene!");
        scaleneTest(0, 0, 0, false, "shouldn't be scalene!");
        permutedScaleneTest(-5, 4, 2, false, "shouldn't be scalene!");
        permutedScaleneTest(-5, -1, 2, false, "shouldn't be scalene!");
        permutedScaleneTest(-1, -2, -3, false, "shouldn't be scalene!");
        permutedScaleneTest(1, 2, 3, false, "shouldn't be scalene!");
        permutedScaleneTest(1, 1, 9, false, "shouldn't be scalene!");
      }
    
      @Test
      public void testIsEquilateral() {
        Triangle t = new Triangle(4, 5, 6); // Not equilateral
        assertFalse("(4, 5, 6) unexpectedly equilateral!", t.isEquilateral());
        t = new Triangle(1, 1, 1); // Equilateral
        assertTrue("(1, 1, 1) unexpectedly not equilateral!", t.isEquilateral());
        t = new Triangle(0, 0, 0); // Not a triangle
        assertFalse("(0, 0, 0) unexpectedly equilateral!", t.isEquilateral());
      }
    
    }
    

    Also notice that there's a bit of a problem here: testIsScalene() fails on the tests for triangle (4, 4, 2). An exception is thrown at this point, so none of the testIsScalene()tests after the (4, 4, 2) test are executed. That's a risk when bundling a lot of tests up into one method like this. So long as you expect all of the tests to pass, and fix things as soon as one of the tests fails, this isn't too much of a problem. Otherwise failures will prevent other tests from running, so if you do expect quite a few failures and aren't going to fix them quickly, you should only have one test per method.

  2. Add a Triangle.toString() method: Here are the modifications you could make to add a toString() method to Triangle.java. Note the addition of the description field so we remember the order of the side lengths that the user called the constructor with (otherwise, since we re-order the sides so that the longest one is stored in p, we wouldn't be able to differentiate between Triangle(4, 5, 6) and Triangle(4, 6, 5) for example).

    Triangle.java: toString() method.
    …
      private int r;
    
      private String description;
    
      public String toString() {
        return description;
      }
    
      public Triangle(int s1, int s2, int s3) {
        description = "Triangle (" + s1 + ", " + s2 + ", " + s3 + ")";
        // Ensure that p is the largest of the three.
        if(s1 > s2) {
    …
    

    Once we've made the above changes to Triangle.java, we can tidy up TriangleTestJ4.java a little:

    TriangleTestJ4.java: after writing Triangle.toString().
    …
      private void scaleneTest(int p, int q, int r, boolean scalene, String m) {
        Triangle t = new Triangle(p, q, r);
        m = t + " " + m;
        assertEquals(m, t.isScalene(), scalene);
      }
    …
      @Test
      public void testIsEquilateral() {
        Triangle t = new Triangle(4, 5, 6); // Not equilateral
        assertFalse(t + " unexpectedly equilateral!", t.isEquilateral());
        t = new Triangle(1, 1, 1); // Equilateral
        assertTrue(t + " unexpectedly not equilateral!", t.isEquilateral());
        t = new Triangle(0, 0, 0); // Not a triangle
        assertFalse(t + " unexpectedly equilateral!", t.isEquilateral());
      }
    …
    

    Notice how we can now just add t to any string we construct (in the messages for the assert calls, in particular) and it'll automatically get converted to a nice text form using the new toString() method.

  3. Fix the implementation: Recall that the definition of a scalene triangle is one all of whose sides have different lengths. The isScalene() implementation you have makes six tests against p, q and r:
      p > 0 && q > 0 && r > 0 && p < q + r && (q < r || q > r)
    

    The first three tests are just to check that all of the sides have a positive length. The fourth one, p < q + r, checks that the sides can make a triangle: translate it into words, remembering that p is chosen to be the longest side by the Triangle constructor, and it says “the length of the longest side is less than the sum of the lengths of the other two sides” — if this wasn't true, then the two short edges wouldn't between them be able to reach from one end of the long edge to the other, so they couldn't possibly make a triangle! So the first four tests are really just about making sure that p, q and r can make a triangle at all.

    The fifth and sixth tests then should be the ones that really test to see if we've got a scalene triangle. But what are they doing? Checking to see that q < r or q > r? That only ensures that the two shorter edges have different lengths, but makes no such statement about their relationship with the long edge. So this is the error. We need to replace them with a test to see that p, q and r are all different — for example:

      (p != q && p != r && q != r)
    

    After this change, at last our scalene tests pass:

    Screenshot of JUnit results with a success tick for testIsScalene

  4. Enhance the implementation: You were asked to make the Triangle constructor throw an exception if it's given an invalid set of parameters. First we need to write the exception class; it's pretty straightforward:

    BadTriangleException.java
    package st;
    
    public class BadTriangleException extends RuntimeException {
    
      public BadTriangleException(String m) {
        super(m);
      }
    
    }
    

    The constructor allows us to add an explanatory message when we throw this exception. Note that BadTriangleException inherits from RuntimeException, which means that you don't need to add any throws clauses for it. Unchecked exceptions are a cause of much debate though, so don't do this without understanding the argument.

    (Aside: the eagle-eyed among you might notice a warning from Eclipse: “The serializable class BadTriangleException does not declare a static final serialVersionUID field of type long.” If you want you can autocorrect it (“Add generated serial version ID”); this page on Java serialization will tell you more…)

    Now that we detect bad triangles in Triangle's constructor, isScalene() doesn't need its first four conditions (which check that the triangle is valid), and many of our tests in testIsScalene and testIsEquilateral have become redundant — these should now be moved to testTriangle, which tests the constructor (more on that later). Here are the new versions of all three methods:

    New version of Triangle.isScalene()
      public boolean isScalene() {
        return p != q && p != r && q != r;
      }
    
    New versions of TriangleTestJ4.testIsScalene() and testIsEquilateral()
      // Most of these tests are inspired by the Tutorial 1 quiz
      @Test
      public void testIsScalene() {
        permutedScaleneTest(4, 5, 6, true, "should be scalene!");
        permutedScaleneTest(4, 3, 2, true, "should be scalene!");
        // No permutations needed for this one...
        scaleneTest(9, 9, 9, false, "shouldn't be scalene!");
        permutedScaleneTest(4, 4, 2, false, "shouldn't be scalene!");
      }
    
      @Test
      public void testIsEquilateral() {
        Triangle t = new Triangle(4, 5, 6); // Not equilateral
        assertFalse(t + " unexpectedly equilateral!", t.isEquilateral());
        t = new Triangle(1, 1, 1); // Equilateral
        assertTrue(t + " unexpectedly not equilateral!", t.isEquilateral());
      }
    
  5. Test the enhancements: Finally, we'll test the “valid triangle” test in Triangle's constructor. JUnit 4 provides a nice annotation for expected exceptions, but remember that if a method throws an exception, then no further code in it will be executed. Consequently, we need to test each kind of bad triangle in its own individual test:

    More tests to add to TrianglTestJ4.java, to see that exceptions are thrown for bad triangles:
      @Test(expected=st.BadTriangleException.class)
      public void testTriangleFirstIsZero() {
        new Triangle(0, 1, 1);
      }
    
      @Test(expected=st.BadTriangleException.class)
      public void testTriangleSecondIsZero() {
        new Triangle(1, 0, 1);
      }
    
      @Test(expected=st.BadTriangleException.class)
      public void testTriangleThirdIsZero() {
        new Triangle(1, 1, 0);
      }
    
      @Test(expected=st.BadTriangleException.class)
      public void testTriangleSidesTooShort() {
        new Triangle(1, 1, 2);
      }
    

    We could also use something like the code at the end of the JUnit 3 version of the first tutorial in order to “wrap” these tests inside another method, and execute them more compactly from a single test method (via a parameter-rotating method, just as we did with permutations for isScalene()):

    Another way of testing to see that exceptions are thrown:
      // Make sure that the constructor throws a BadTriangleException
      // when given a particular set of parameters.
      private void assertConstructorException(int p, int q, int r) {
        String m = "Triangle (" + p + ", " + q + ", " + r + ") ";
        try {
          new Triangle(p, q, r);
          fail(m + "didn't throw a BadTriangleException!");
        } catch(BadTriangleException e) {
          return;
        } catch(Throwable t) {
          fail(m + "threw a " + t + ", not a BadTriangleException!");
        }
      }
    
      // This tests all three rotations of p, q, r, to see that each one
      // causes an exception.
      private void rotatingAssertConstructorException(int p, int q, int r) {
        assertConstructorException(p, q, r);
        assertConstructorException(q, r, p);
        assertConstructorException(r, p, q);
      }
    
      // Test constructor to see that bad triangles are rejected.
      @Test
      public void testTriangle() {
        rotatingAssertConstructorException(0, 1, 1);
        rotatingAssertConstructorException(-1, 1, 1);
        rotatingAssertConstructorException(2, 1, 1);
        rotatingAssertConstructorException(3, 1, 1);
      }
    

    That's it: we've now got a reasonable set of tests for our constructor, and both isScalene() and isEquilateral().


Version 1.2, 2011/01/14 01:02:24


Home : Teaching : Courses : St : 2010-2011 

Informatics Forum, 10 Crichton Street, Edinburgh, EH8 9AB, Scotland, UK
Tel: +44 131 651 5661, Fax: +44 131 651 1426, E-mail: school-office@inf.ed.ac.uk
Please contact our webadmin with any comments or corrections. Logging and Cookies
Unless explicitly stated otherwise, all material is copyright © The University of Edinburgh