Skip to main content

Tutorial: Using Symflower for Test-Driven Development

This example project shows you how to use Symflower to boost your efficiency in a TDD environment with generated test templates.

The running example

In this example, you're going to build an application that helps manage and classify triangles using Test-Driven Development.

Specification: You want to be able to store the length of each side of the triangle, and you also want the application to tell which kind of triangle you're dealing with:

  • Equilateral: all sides are the same length
  • Isosceles: two sides have the same length
  • Scalene: none of the sides are equal in length

Solution: Let's start by implementing a class in TriangleType.java that defines the above types of triangles, plus an invalid type:

public enum TriangleType {
equilateral,
isosceles,
scalene,
invalid
}

In a new Triangle.java file, you're just going to start with an empty triangle class, leaving all the logic for later. First, you need to define the signature for the function you want to implement, which is getType:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public TriangleType getType(){}
}

Once that signature is in place, it's time to start writing unit tests. Here's where you start saving manual effort by having Symflower generate a smart unit test template. Right-click on the function and select Symflower: Generate Test Template for Function (alternatively, you can use the code lens, your configured hotkey, or the command palette). This action will yield the following smart test template:

public class TriangleTest {
@Test
public void getType() {
Triangle t = new Triangle();
TriangleType expected = TriangleType.equilateral;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}
}

The smart test template initializes a triangle, where all sides are set to 0, i.e. you have an invalid triangle. Let's edit the proposed test template accordingly:

@Test
public void getType() {
Triangle t = new Triangle();
TriangleType expected = TriangleType.invalid;
TriangleType actual = t.getType();


assertEquals(expected, actual);
}

By running the test, you quickly find that the project can't be compiled because getType doesn't return any triangle type. Let's change the code to return the invalid triangle type and try again!

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public TriangleType getType() {
return TriangleType.invalid;
}
}

Everything works now, the test passes. Let's now move on to writing another test for the scenario where we have an equilateral triangle. To save time, you're again going to rely on Symflower to generate the test template. Right-click on the function and select Symflower: Generate Test Template for Function (alternatively, you can use the code lens, your configured hotkey, or the command palette).

At this point, it makes sense to rename both tests and to fill up the getTypeEquilateral test case with the appropriate values:

public class TriangleTest {
@Test
public void getTypeInvalid() {
Triangle t = new Triangle();
TriangleType expected = TriangleType.invalid;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}

@Test
public void getTypeEquilateral() {
Triangle t = new Triangle(1, 1, 1);
TriangleType expected = TriangleType.equilateral;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}
}

Since the Triangle class doesn't currently have a constructor that takes any arguments, this latter test would fail. So let's add that now:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public Triangle(int sideA, int sideB, int sideC) {
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}

public TriangleType getType() {
return TriangleType.invalid;
}
}

Running the test now leads to another error about the value not being used. That means you'll have to adapt the first test:

public class TriangleTest {
@Test
public void getTypeInvalid() {
Triangle t = new Triangle(0, 0, 0);
TriangleType expected = TriangleType.invalid;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}

@Test
public void getTypeEquilateral() {
Triangle t = new Triangle(1, 1, 1);
TriangleType expected = TriangleType.equilateral;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}
}

One of your tests now fails because it expected equilateral as output, but got invalid. That means you need to adapt your code to make this test pass:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public Triangle(int sideA, int sideB, int sideC) {
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}

public TriangleType getType(){
if (sideA == sideB && sideB == sideC) {
return TriangleType.equilateral
}
return TriangleType.invalid;
}
}

There's something wrong now. The second test passes but the first one returns equilateral instead of invalid. So you have to add a special check for the case that one or more sides are of length 0:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public Triangle(int sideA, int sideB, int sideC) {
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}

public TriangleType getType() {
if (sideA <= 0 || sideB <= 0 || sideC <=0){
return TriangleType.invalid
}

if (sideA == sideB && sideB == sideC) {
return TriangleType.equilateral
}
return TriangleType.invalid;
}
}

Running the tests now, you can see that everything works fine. Let's continue! Next up, you'll have Symflower generate another smart test template and adapt the test for an isosceles triangle. Here's the test you'll add to your test file:

@Test
public void getTypeIsosceles() {
Triangle t = new Triangle(1, 2, 2);
TriangleType expected = TriangleType.isosceles;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}

The test, of course, fails, so let's adapt the implementation again:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public Triangle(int sideA, int sideB, int sideC) {
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}

public TriangleType getType() {
if (sideA <= 0 || sideB <= 0 || sideC <=0){
return TriangleType.invalid;
}

if (sideA == sideB && sideB == sideC) {
return TriangleType.equilateral;
}

if (sideA == sideB || sideB == sideC || sideA == sideC){
return TriangleType.isosceles;
}

return TriangleType.invalid;
}
}

Upon running the test suite, you should see that everything passes. Let's move on to adding a test for a scalene triangle. Once again, you'll just trigger Symflower's smart test template generation, and edit the test template as follows:

@Test
public void getTypeScalene() {
Triangle t = new Triangle(1, 2, 3);
TriangleType expected = TriangleType.scalenes;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}

The new test fails alright, so let's adapt our implementation again:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public Triangle(int sideA, int sideB, int sideC) {
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}

public TriangleType getType() {
if (sideA <= 0 || sideB <= 0 || sideC <=0){
return TriangleType.invalid;
}

if (sideA == sideB && sideB == sideC) {
return TriangleType.equilateral;
}

if (sideA == sideB || sideB == sideC || sideA == sideC){
return TriangleType.isosceles;
}

return TriangleType.scalene;
}
}

At this point, you've covered all the triangle types. When you run the test suite now, you'll see that all of your tests pass!

Finding edge cases

It can be hard to think of all the edge cases that could potentially come up. Symflower can help with that, too.

Using Symflower to Generate Test Suite for Function (BETA) creates a new file with a test suite that applies calculated values to cover all potential paths in the code. You can easily review the generated tests and choose which ones you'd like to add to your test suite.

This could help you notice unexpected behavior that you may need to investigate further. For instance, here's one of the test cases generated by Symflower:

@Test
public void getType6() {
Triangle t = new Triangle(1607729662, 2, 1);
TriangleType expected = TriangleType.scalene;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}

You'll notice that something odd is happening: side A is bigger than side B and side C combined. This should of course result in an invalid triangle, meaning that our current implementation is wrong. So let's add this test case and adapt it to fit our specification:

@Test
public void getType6() {
Triangle t = new Triangle(1607729662, 2, 1);
TriangleType expected = TriangleType.invalid;
TriangleType actual = t.getType();

assertEquals(expected, actual);
}

The test fails now, meaning you have to add this edge case to your implementation:

public class Triangle {
private int sideA;
private int sideB;
private int sideC;

public Triangle(int sideA, int sideB, int sideC) {
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}

public TriangleType getType() {
if (sideA <= 0 || sideB <= 0 || sideC <=0){
return TriangleType.invalid;
}

if (sideA + sideB < sideC || sideA + sideC < sideB || sideB + sideC < sideA){
return TriangleType.invalid;
}

if (sideA == sideB && sideB == sideC) {
return TriangleType.equilateral;
}

if (sideA == sideB || sideB == sideC || sideA == sideC){
return TriangleType.isosceles;
}

return TriangleType.scalene;
}
}

Now, the test suite runs through again.

Pro tip: Once you've generated a test suite with Symflower, it will highlight potential problems in your code with red wiggly lines. You can also set up Symflower to generate tests in the background, keeping your test suite maintained and pinpointing flaws in your code in real time.