Lesson Tuesday

In the previous lesson we focused exclusively on encapsulation and visibility and how to write/use getter methods. 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.

However, during the course of developing an actual application, it is a good idea to test each method using JUnit. The issue of whether or not you should be writing tests for getter methods is not without controversy. It is true that a simple getter or setter that does not contain any logic should not fail for an experienced developer. But, for novice programmers such as yourself, those new to practicing the sequence of BDD, and if your getters contain any kind of logic at all (such as concatenation), it is a good idea to explicitly test your getters (and setters). If a simple getter is not written correctly and returns the wrong value, even a complex application will never run correctly or reliably.

We will run through an explicit sequence of testing getter methods here. Once you have more experience developing applications and writing automated tests, you can decide for yourself whether this is the area where you want to cut corners. If you are working in a development team that practices BDD, always confer with your supervisor first before omitting tests for getters and setters.

Rectangle Application

Let's create an application that will ask for the height 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. We'll pick this app back up at a later date, slightly further into the course, as well, and will build out our frontend for this at that point.

Setup

We'll need to set up our basic project directory before we begin. Call it rectangle. Create a package called models in src/main/java, and create a Rectangle.java class inside that folder.

Generate a default constructor that takes no arguments.

Generate a test file for Rectangle by clicking inside the Rectangle class file's curly braces and pressing shift + alt + t on our classroom macs (shift + command + t or shift + ctrl + t on windows).

Leave all the default settings as is.

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. As you can see, we can instantiate an Object in our test to ensure that it is actually getting created the way we expect.

Right click in your test file and select Generate > Test Method

RectangleTest.java
package models;

import org.junit.Test;
import static org.junit.Assert.*;

public class RectangleTest {

   @Test
   public void newRectangle_instantiatesCorrectly() throws Exception {
       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 our test by right clicking on the editor tab of our test and selecting Run we should see:

Error:(14, 35) java: constructor Rectangle in class models.Rectangle cannot be applied to given types;
  required: no arguments
  found: int,int
  reason: actual and formal argument lists differ in height

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, when we run our test, Gradle 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 height and width), we'll create an empty constructor:

rectangle/src/main/java/Rectangle.java
package models;

public class Rectangle {

   public Rectangle(int height, int width) {
   }
}

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

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 height and width values of each Rectangle as properties.

First, let's specifically tests that it can save height 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_getsHeight_2() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(2, testRectangle.getHeight());
  }

}

Because we need to get the rectangle's height in order to confirm that the constructor saved the height value correctly, this test will also eventually confirm whether our getHeight() 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 our test again, we receive a compiler error:

Error:(21, 38) java: cannot find symbol
  symbol:   method getHeight()
  location: variable testRectangle of type models.Rectangle

The error message reads cannot find symbol, and points to the getHeight() method. This is correct, because we haven't yet defined getHeight(). 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 height, int width) {

  }

  public int getHeight() {
    return 0;
  }
}

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

java.lang.AssertionError:
Expected :2
Actual   :0
 <Click to see difference>


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 height value provided in the constructor to a member variable, then return the member variable upon request with the getHeight() getter method.

First, let's declare an height 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
package models;

public class Rectangle {

   private int height;

   public Rectangle(int height, int width) {
       this.height = height;
   }

   public int getHeight(){
       return 0;
   }
}

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

rectangle/src/main/java/Rectangle.java
package models;

public class Rectangle {

   private int height;

   public Rectangle(int height, int width) {
       this.height = height;
   }

   public int getHeight(){
       return this.height;
   }
}

Then, we'll code our getHeight() method to return the height variable:

rectangle/src/main/java/Rectangle.java
package models;

public class Rectangle {

   private int height;

   public Rectangle(int height, int width) {
       this.height = height;
   }

   public int getHeight(){
       return this.height;
   }
}

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_getsHeight_2() {
    Rectangle testRectangle = new Rectangle(2, 4);
    assertEquals(2, testRectangle.getHeight());
  }

 @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:

Error:(24, 38) java: cannot find symbol
  symbol:   method getWidth()
  location: variable testRectangle of type models.Rectangle

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

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

  public Rectangle(int height, int width) {
    height = height;
  }

  public int getHeight() {
    return height;
  }

  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 width 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 width value in the getWidth() method:

rectangle/src/main/java/Rectangle.java
package models;

public class Rectangle {

   private int height;
   private int width;

   public Rectangle(int height, int width) {
       this.height = height;
       this.width = width;
   }

   public int getHeight(){
       return this.height;
   }

   public int getWidth(){
       return this.width;
   }

}

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 height 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 height and width attributes to determine if it is a square:

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

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 height and width 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.

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
package models;

import org.junit.Test;

import static org.junit.Assert.*;

public class RectangleTest {

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

   @Test
   public void newRectangle_getsHeight_2() throws Exception {
       Rectangle testRectangle = new Rectangle(2, 4);
       assertEquals(2, testRectangle.getHeight());
   }

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

   }

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

   }

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

}

Rectangle.java:

rectangle/src/main/java/Rectangle.java
package models;

public class Rectangle {

   private int height;
   private int width;

   public Rectangle(int height, int width) {
       this.height = height;
       this.width = width;
   }

   public int getHeight(){
       return this.height;
   }

   public int getWidth(){
       return this.width;
   }

   public boolean isSquare() {
       return height == width;
   }

}