Test-Driven Development
This example project shows you how to use Symflower to boost your efficiency in a TDD environment with generated test templates.
Tutorial project
In this tutorial, 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
Step 1: Define triangle types
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
}
Step 2: Create a triangle class
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(){}
}
Step 3: Generate a test template
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 any of the available usage options). 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);
}
}
Step 4: Implement the function
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;
}
}
Step 5: Implement the rest of the function
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, the code doesn't compile. 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;
}
}
Run the tests again. 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.scalene;
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 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, test-backed diagnostics will highlight potential problems in your code with red wiggly lines. You can also configure Symflower to generate tests in the background, keeping your test suite maintained and pinpointing flaws in your code in real time.