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.
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.
We'll need to set up our basic project directory before we begin.
Your directory structure should look like this:
rectangle
├── build.gradle
└── src
├── main
│ └── java
│ └── Rectangle.java
└── test
└── java
└── RectangleTest.java
apply plugin: 'java'
apply plugin: 'application'
archivesBaseName = "rectangle"
version = '1.0'
mainClassName = "Rectangle"
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.+'
}
Let's declare our empty Rectangle
class:
public class Rectangle {
}
We'll also set up our test file:
import org.junit.*;
import static org.junit.Assert.*;
public class RectangleTest {
}
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.
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.
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:
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.
Rectangle
called testRectangle
. assertEquals()
method.
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. 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. 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 int
s 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:
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:
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.
The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest 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.
The second step in Red, Green, Refactor: Write a coded test.
We'll add a spec for this behavior:
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.
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
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:
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:
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:
public class Rectangle {
private int mLength;
public Rectangle(int length, int width) {
mLength = length;
}
public int getLength() {
return mLength;
}
}
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!
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.
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.
The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest 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.
The second step in Red, Green, Refactor: Write a coded test.
We'll add a new test:
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());
}
}
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
:
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.
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:
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;
}
}
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!
The sixth step in Red, Green, Refactor: Confirm all previous tests still pass.
Additionally, all previous tests should still be pass. Perfect!
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.
The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest 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.
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:
...
@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:
...
public boolean isSquare() {
return true;
}
...
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!
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:
...
public boolean isSquare() {
return mLength == mWidth;
}
...
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!
The sixth step in Red, Green, Refactor: Confirm all previous tests still pass.
All previous tests pass too!
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.
The eighth and final step in Red, Green, Refactor: Repeat the process with the next simplest 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.
The second step in Red, Green, Refactor: Write a coded test.
We'll write another automated test for this behavior:
...
@Test
public void isSquare_allSidesEqual_true() {
Rectangle testRectangle = new Rectangle(2, 2);
assertEquals(true, testRectangle.isSquare());
}
...
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:
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:
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:
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
:
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:
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:
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:
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.
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.
Example test file for an application with custom objects, and getter methods:
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());
}
}