In the previous lesson, we focused exclusively on encapsulation and visibility and how to write/use getter methods. However, during the course of developing an actual application, you'll need to test each method using JUnit.

Now that we know about encapsulation and visibility, and we've practiced writing automated JUnit tests as part of the Red, Green, Refactor workflow, let's combine the two. In this lesson, we'll walk through creating a small application that uses custom classes to create objects. We'll fully encapsulate our class by declaring all properties private. Then, to access information about our objects, we'll define getter methods.

Rectangle Application

Let's create an application that will ask for the length and width of a rectangle. The application will use this information to create a new Rectangle object. Then, we'll define a method to evaluate the Rectangle and inform the user whether or not their rectangle is also a square.

Setup

We'll need to set up our basic project directory before we begin.

Project Directory

  • Make a folder for the project called rectangle.
  • Within the rectangle directory, create a src subdirectory for your source code.
  • In the src subdirectory, create main and test subdirectories. As you know, these will contain the main application logic and test files, respectively.
  • In both the main and test subdirectories, create an additional java subdirectory.
  • in the rectangle/src/main/java directory, create a Rectangle.java file to contain the back-end logic.
  • In the rectangle/src/test/java directory, create a RectangleTest.java file. It will house the JUnit tests for our back-end logic.
  • In the top-level of the rectangle directory, create a build.gradle file.

Your directory structure should look like this:

rectangle
├── build.gradle
└── src
    ├── main
    │   └── java
    │       └── Rectangle.java
    └── test
        └── java
            └── RectangleTest.java

build.gradle File

rectangle/build.gradle
apply plugin: 'java'
apply plugin: 'application'

archivesBaseName = "rectangle"
version = '1.0'
mainClassName = "Rectangle"

repositories {
  mavenCentral()
}

dependencies {
  testCompile group: 'junit', name: 'junit', version: '4.+'
}

Class Files

Let's declare our empty Rectangle class:

rectangle/src/main/java/Rectangle.java
public class Rectangle {

}

Test Files

We'll also set up our test file:

rectangle/src/test/java/RectangleTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class RectangleTest {

}

Behavior-Driven Development with Objects and Encapsulation

Great! Now that everything is in place, we can begin to develop our application. As always, we'll follow the Red, Green, Refactor workflow. If you're not yet 100% comfortable with this process, feel free to keep this lesson open as you follow along.

Identify Simplest Behavior

The first step in Red, Green, Refactor: Identify the program's simplest possible behavior.

As you know, we first need to identify the simplest behavior our application must exhibit. Remember, the purpose of this program is to gather two values from the user, create a Rectangle object with them, then evaluate whether the Rectangle is also a square.

Therefore, simplest behavior it will have to demonstrate is creating an instance of the Rectangle class. Whenever you're developing a program containing custom objects using Behavior-Driven Development, testing the constructor in this fashion will usually be the first behavior in your Red, Green, Refactor workflow.

Writing a Coded Test

The second step in Red, Green, Refactor: Write a coded test.

Before we begin coding, let's write an automated JUnit test for the behavior we've just identified:

RectangleTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class RectangleTest {

  @Test
  public void newRectangle_instantiatesCorrectly() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(true, testRectangle instanceof Rectangle);
  }
}   

Here we are testing whether we can successfully create a new instance of the Rectangle class.

  • We do so by creating a Rectangle called testRectangle.
  • Then, we pass in two arguments to the assertEquals() method.
    • The second is testRectangle instanceof Rectangle. instanceof will evaluate our testRectangle object, and return true or false depending on whether testRectangle is an instance of our Rectangle class.
    • The first argument is true, which is what we anticipate testRectangle instanceof Rectangle should return. If it returns true, we know testRectangle is an instance of the Rectangle class, and that our application is successfully creating Rectangle objects.

Make Sure the Test Fails

The third step in Red, Green, Refactor: Before coding, make sure the test fails.

When we run $ gradle test we should see:

 $ gradle test

Starting a Gradle Daemon (subsequent builds will be faster)
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
/Users/staff/Desktop/java/rectangle/src/test/java/RectangleTest.java:8: error: constructor Rectangle in class Rectangle cannot be applied to given types;
    Rectangle testRectangle = new Rectangle(2, 4);
                              ^
  required: no arguments
  found: int,int
  reason: actual and formal argument lists differ in length
1 error
:compileTestJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileTestJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 5.169 secs

Similar to what we saw in this lesson, this is not a test failure message, but a compiler error.

The error reads constructor Rectangle in class Rectangle cannot be applied to given types; This is because the default constructor for Java classes does not accept arguments. But our test attempts to provide two arguments in the line: Rectangle testRectangle = new Rectangle(2, 4); The compiler therefore informs us that the constructor required: no arguments but that it found: int,int. In other words, we passed it two ints when it was expecting nothing.

Remember, $ gradle test first compiles source code, then runs JUnit tests. If the application cannot compile, JUnit tests never run. So, we must address compiler errors before our test can fail appropriately.

Since we will initialize every Rectangle with two arguments (a length and width), we'll create an empty constructor:

rectangle/src/main/java/Rectangle.java
public class Rectangle {

  public Rectangle(int length, int width) { 

  }

}

If we run $ gradle test again, it compiles the project successfully, and our JUnit test passes!

$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

BUILD SUCCESSFUL

Total time: 1.748 secs

But wait, don't we want it to fail? Similar to what we saw when we created a Leap Year application using BDD, tests will unexpectedly pass sometimes. When they do, take a careful look at the test and any logic your program contains to ensure something isn't wrong.

In our case, if we don't create a constructor in Rectangle.java, the project will not compile. Yet, if we do create a constructor, the test passes instantly because it only asserts whether testRectangle is an instanceof the Rectangle class. So, this test passing is actually okay.

This will often occur when using instance of to test constructors. Just always make sure to take a careful look and confirm the test is passing for the 'right' reasons.

Since this behavior is so simple that getting past the compiler errors actually results in passing the test, we've already completed the following Red, Green, Refactor steps:

  1. Implement the behavior with the least amount of code possible.
  2. Run the automated test to confirm it passes.
  3. Make sure all previous tests still pass.

Refactor

The seventh step in Red, Green, Refactor: Check if you can refactor. If so, refactor and confirm all tests still pass.

Our logic is really, really DRY. There isn't room for refactoring yet.

Repeat

The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest behavior.

Identify a Behavior

The first step in Red, Green, Refactor: Identify the program's simplest possible behavior.

Now that we've confirmed our program can successfully create a Rectangle, the next simplest behavior will be saving the length and width values of each Rectangle as properties.

First, let's specifically tests that it can save length values.

Write a Coded Test

The second step in Red, Green, Refactor: Write a coded test.

We'll add a spec for this behavior:

rectangle/src/test/java/RectangleTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class RectangleTest {

  @Test
  public void newRectangle_instantiatesCorrectly() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(true, testRectangle instanceof Rectangle);
  }

  @Test
  public void newRectangle_getsLength_2() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(2, testRectangle.getLength());
  }

}

Because we need to get the rectangle's length in order to confirm that the constructor saved the length value correctly, this test will also eventually confirm whether our getLength() getter method is functioning correctly. Every getter method you define needs to be tested.

Make Sure the Test Fails

The third step in Red, Green, Refactor: Before coding, make sure the test fails.

If we run $ gradle test again, we receive a compiler error:

$ gradle test

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
/Users/staff/Desktop/java/rectangle/src/test/java/RectangleTest.java:15: error: cannot find symbol
    assertEquals(2, testRectangle.getLength());
                                 ^
  symbol:   method getLength()
  location: variable testRectangle of type Rectangle
1 error
:compileTestJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileTestJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.651 secs

The error message reads cannot find symbol, and points to the getLength() method. This is correct, because we haven't yet defined getLength(). Let's declare it now, and have it temporarily return 0 in order to get past compiler errors and view our test failure message:

public class Rectangle {

  public Rectangle(int length, int width) {

  }

  public int getLength() {
    return 0; 
  }

}

If we run the test again, we'll receive our proper JUnit test failure message:

$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

RectangleTest > newRectangle_getsLength_2 FAILED
    java.lang.AssertionError at RectangleTest.java:15

2 tests completed, 1 failed
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/staff/Desktop/java/rectangle/build/reports/tests/test/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 1.507 secs

rectangles-first-failing-test

Implement the Behavior

The fourth step in Red, Green, Refactor: Implement the behavior with the least amount of code possible.

Great! Now we can add logic to save the length value provided in the constructor to a member variable, then return the member variable upon request with the getLength() getter method.

First, let's declare an mLength property on the Rectangle class. Remember, this variable should be declared as private. We want everything encapsulated within our Rectangle class:

rectangle/src/main/java/Rectangle.java
public class Rectangle {
  private int mLength;

  public Rectangle(int length, int width) {

  }

  public int getLength() {
    return 0;
  }

}

Next, we'll set this attribute with the value passed into the constructor:

rectangle/src/main/java/Rectangle.java
public class Rectangle {
  private int mLength;

  public Rectangle(int length, int width) {
    mLength = length; 
  }

  public int getLength() {
    return 0;
  }

}

Then, we'll code our getLength() method to return the mLength variable:

rectangle/src/main/java/Rectangle.java
public class Rectangle {
  private int mLength;

  public Rectangle(int length, int width) {
    mLength = length;
  }

  public int getLength() {
    return mLength;
  }

}

Run the Automated Test(s)

The fifth step in Red, Green, Refactor: Run tests to confirm the new spec passes.

If we run our tests again, our new test passes!

Make Sure Previous Tests Pass

The sixth step in Red, Green, Refactor: Confirm all previous tests still pass.

Our previous test passes, too! We can advance to the next step.

Refactor

The seventh step in Red, Green, Refactor: Check if you can refactor. If so, refactor and confirm all tests still pass.

No room for refactoring yet. We can continue moving forward.

Repeat

The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest behavior.

Identify a Behavior

The first step in Red, Green, Refactor: Identify the program's simplest possible behavior.

Next, let's make sure Rectangle objects can save width values too, and that a getter method can successfully access and return them to us. This will be very similar to the behavior we just implemented.

Write a Coded Test

The second step in Red, Green, Refactor: Write a coded test.

We'll add a new test:

rectangle/src/test/java/RectangleTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class RectangleTest {

  @Test
  public void newRectangle_instantiatesCorrectly() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(true, testRectangle instanceof Rectangle);
  }

  @Test
  public void newRectangle_getsLength_2() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(2, testRectangle.getLength());
  }

 @Test
  public void getWidth_getsRectangleWidth_4() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(4, testRectangle.getWidth());
  }

}

Make Sure the Test Fails

The third step in Red, Green, Refactor: Before coding, make sure the test fails.

We'll run our tests again to confirm the latest spec fails. Similar to last time, we'll receive a compiler error alerting us getWidth() is not defined:

$ gradle test
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava
/Users/staff/Desktop/java/rectangle/src/test/java/RectangleTest.java:21: error: cannot find symbol
    assertEquals(4, testRectangle.getWidth());
                                 ^
  symbol:   method getWidth()
  location: variable testRectangle of type Rectangle
1 error
:compileTestJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileTestJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.632 secs

We'll declare a getWidth() method with a temporary return value of 0:

rectangle/src/main/java/Rectangle.java
public class Rectangle {
  private int mLength;

  public Rectangle(int length, int width) {
    mLength = length;
  }

  public int getLength() {
    return mLength;
  }

  public int getWidth() {
    return 0;
  }

}

If we run our tests again, we should receive the appropriate failure message.

Implement the Behavior

The fourth step in Red, Green, Refactor: Implement the behavior with the least amount of code possible.

Next, let's add the minimum amount of code to pass this test. In our case, we'll declare a private mWidth property on the Rectangle class, and make sure the constructor sets this property with the provided width argument. Then, we'll add logic to return the private mWidth value in the getWidth() method:

rectangle/src/main/java/Rectangle.java
public class Rectangle {
  private int mLength;
  private int mWidth;

  public Rectangle(int length, int width) {
    mLength = length;
    mWidth = width;
  }

  public int getLength() {
    return mLength;
  }

  public int getWidth() {
    return mWidth;
  }

}

Run the Automated Tests

The fifth step in Red, Green, Refactor: Run tests to confirm the new spec passes.

If we run the tests again, the new spec passes!

Make Sure Previous Tests Pass

The sixth step in Red, Green, Refactor: Confirm all previous tests still pass.

Additionally, all previous tests should still be pass. Perfect!

Refactor

The seventh step in Red, Green, Refactor: Check if you can refactor. If so, refactor and confirm all tests still pass.

Next, we'll see if we can refactor. Since we've been adding the smallest amount of code possible when implementing each behavior, there isn't room for refactoring. But, as always, we should double-check anyway.

Repeat

The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest behavior.

Identify a Behavior

The first step in Red, Green, Refactor: Identify the program's simplest possible behavior.

Now that we can successfully create Rectangle objects with the necessary information, the next behavior we'll need to implement is identifying when a Rectangle isn't a square.

Write a Coded Test

The second step in Red, Green, Refactor: Write a coded test.

First let's write a test to make sure the program identifies that a Rectangle with a length of 2 and width of 4 is not a square:

rectangle/src/test/java/RectangleTest.java
...
@Test
public void isSquare_whenNotASquare_false() {
  Rectangle testRectangle = new Rectangle(2, 4);
  assertEquals(false, testRectangle.isSquare());
}
...

Back in our Rectangle class we'll declare the isSquare() method with a temporary return value to move beyond compiler errors:

rectangle/src/main/java/Rectangle.java
...
  public boolean isSquare() {
    return true;
  }
...

Make Sure the Test Fails

The third step in Red, Green, Refactor: Before coding, make sure the test fails.

If we run our tests again, the latest test should correctly fail. Great!

Implement the Behavior

The fourth step in Red, Green, Refactor: Implement the behavior with the least amount of code possible.

Next, let's add code to our isSquare() method to accurately evaluate the Rectangle object's mLength and mWidth attributes to determine if it is a square:

rectangle/src/main/java/Rectangle.java
...
public boolean isSquare() {
  return mLength == mWidth;
}
...

Run the Automated Tests

The fifth step in Red, Green, Refactor: Run tests to confirm the new spec passes.

If we run our tests again, the latest spec should now pass!

Make Sure Previous Tests Pass

The sixth step in Red, Green, Refactor: Confirm all previous tests still pass.

All previous tests pass too!

Refactor

The seventh step in Red, Green, Refactor: Check if you can refactor. If so, refactor and confirm all tests still pass.

Again, no room for refactoring quite yet. But this will change when we begin developing more complex applications.

Repeat

The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest behavior.

Identify a Behavior

The first step in Red, Green, Refactor: Identify the program's simplest possible behavior.

There is only one last behavior our program must demonstrate. In addition to recognizing when a Rectangle is not a square, we need to confirm it can recognize when a Rectangle is a square.

Write a Coded Test

The second step in Red, Green, Refactor: Write a coded test.

We'll write another automated test for this behavior:

rectangle/src/test/java/RectangleTest.java
...
@Test
public void isSquare_allSidesEqual_true() {
  Rectangle testRectangle = new Rectangle(2, 2);
  assertEquals(true, testRectangle.isSquare());
}
...

Make Sure the Test Fails

The third step in Red, Green, Refactor: Before coding, make sure the test fails.

When we run our tests again, they all pass! This is because we actually already added the logic for recognizing if a Rectangle has the same values for its mLength and mWidth properties when we implemented the behavior to ensure the application recognizes when a Rectangle is not a square.

Since this behavior was actually already present, and all tests pass, we've completed the following Red, Green, Refactor steps:

  1. Implement the behavior with the least amount of code possible.
  2. Run the automated test to confirm it passes.
  3. Make sure all previous tests still pass.

Front-End User Interface

Now that we've built DRY, well-tested back-end logic, we can add our user interface.

Create an App.java file in the rectangle/src/main/java directory.

Next, add the necessary boilerplate code. We'll also import the Console class and declare an instance of it in order to gather user input:

rectangle/src/main/java/App.java
import java.io.Console;

public class App {
  public static void main(String[] args) {
    Console myConsole = System.console();
  }
}

Next, we'll add prompts to ask user to enter the length and width values of a Rectangle:

rectangle/src/main/java/App.java
import java.io.Console;

public class App {
  public static void main(String[] args) {
    Console myConsole = System.console();
    System.out.println("Enter the length of your rectangle:");
  }
}

We'll gather the user's input and parse it from a String into an Integer:

rectangle/src/main/java/App.java
import java.io.Console;

public class App {
  public static void main(String[] args) {
    Console myConsole = System.console();
    System.out.println("Enter the length of your rectangle:")
    String stringLength = myConsole.readLine();
    int intLength = Integer.parseInt(stringLength);
  }
}

Next, we'll do the same for rectangle's width:

rectangle/src/main/java/App.java
import java.io.Console;

public class App {
  public static void main(String[] args) {
    Console myConsole = System.console();
    System.out.println("Enter the length of your rectangle:");
    String stringLength = myConsole.readLine();
    int intLength = Integer.parseInt(stringLength);
    System.out.println("Enter the width of your rectangle:");
    String stringWidth = myConsole.readLine();
    int intWidth = Integer.parseInt(stringWidth);
  }
}

Finally, we'll create an instance of the Rectangle class with the intLength and intWidth values we collected and parsed:

rectangle/src/main/java/App.java
import java.io.Console;

public class App {
  public static void main(String[] args) {
    Console myConsole = System.console();
    System.out.println("Enter the length of your rectangle:");
    String stringLength = myConsole.readLine();
    int intLength = Integer.parseInt(stringLength);
    System.out.println("Enter the width of your rectangle:");
    String stringWidth = myConsole.readLine();
    int intWidth = Integer.parseInt(stringWidth);
    Rectangle rectangle = new Rectangle(intLength, intWidth);
  }
}

Next, we'll call our isSquare() method upon the new Rectangle to determine whether or not it is also a square. Then, we'll print these results to the user:

rectangle/src/main/java/App.java
import java.io.Console;

public class App {
  public static void main(String[] args) {
    Console myConsole = System.console();
    System.out.println("Enter the length of your rectangle:");
    String stringLength = myConsole.readLine();
    int intLength = Integer.parseInt(stringLength);
    System.out.println("Enter the width of your rectangle:");
    String stringWidth = myConsole.readLine();
    int intWidth = Integer.parseInt(stringWidth);
    Rectangle rectangle = new Rectangle(intLength, intWidth);
    boolean squareResult = rectangle.isSquare();
    System.out.println("Is your rectangle a square, too? " + squareResult + "!");
  }
}

We can use the $ gradle compileJava` command to compile our program:

$ gradle compileJava

:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.729 secs

Next, in a new terminal tab, we'll navigate to the build/classes/main directory containing our Gradle-compiled code:

$ cd build/classes/main

...launch our application:

$ java App

If we provide two of the same values, it should correctly inform us that our rectangle is also a square:

Enter the length of your rectangle:
2

Enter the width of your rectangle:
2

Is your rectangle a square, too? true!

Or, if we provide two different values, it should inform us that our Rectangle is not a square:

Enter the length of your rectangle:
4

Enter the width of your rectangle:
7

Is your rectangle a square, too? false!

Awesome! Moving forward, make sure to declare all object properties as private. Then, as you develop your project, continue following the Red, Green, Refactor workflow to create tests for each getter method created.


Example GitHub Repo for Rectangle Checker

Overview


  • Moving forward, all attributes of our custom classes should be made private instead of public. As discussed in the Encapsulation and Visibility lesson, this means that outside classes will not be able to directly access these member variables (like this: testVehicle.mPrice).

  • Instead, also covered in this lesson, we must create getter and setter methods that return this information for us.

  • Like any other behavior a program demonstrates, all getter and setter methods should be coded using the Red, Green, Refactor workflow. There should be tests in place to confirm that each function appropriately.

Examples


Example test file for an application with custom objects, and getter methods:

rectangle/src/test/java/RectangleTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class RectangleTest {

  @Test
  public void newRectangle_instantiatesCorrectly() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(true, testRectangle instanceof Rectangle);
  }

  @Test
  public void newRectangle_getsLength_2() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(2, testRectangle.getLength());
  }

  @Test
  public void getWidth_getsRectangleWidth_4() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(4, testRectangle.getWidth());
  }

  @Test
  public void isSquare_whenNotASquare_false() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(false, testRectangle.isSquare());
  }

  @Test
  public void isSquare_allSidesEqual_true() {
    Rectangle testRectangle = new Rectangle(2, 2);
    assertEquals(true, testRectangle.isSquare());
  }

}

Example GitHub Repo for Rectangle Checker