Lesson Wednesday

Now that we've covered "Red, Green, Refactor", Gradle, and JUnit, let's create another project together using these tools and workflows. In this lesson we will create a program that returns whether a user-provided year is a leap year.

Again, you are not required to code along with this lesson when completing it as homework. Read through to review the concepts covered here. You'll follow along to code the leap year application with your partner tomorrow.

To internalize the Red, Green, Refactor workflow, keep this lesson open while following along.

Leap Years

Almost every 4 years we experience something called a "leap year". These years are special because they contain 366 days instead of 365. Normally February only has 28 days. During leap years it has 29.

These are the 3 criteria that determine whether a year is a leap year:

  • If a year is evenly divisible by 4 it is a leap year.
  • If a year is divisible by 400 it is a leap year.
  • If a year is evenly divisible by 100 (and not divisible by 400) it is not a leap year.

Our program will use these criteria to evaluate a year, and return whether it is a leap year.

Setup

Before we can write tests we must set up our project. Let's do that now.

Project Directory

As you know, Gradle requires a particular project structure. We'll create this now:

  • Create a leap-year directory.
  • In this directory create a src subdirectory for your source code.
  • In the src subdirectory, create main and test subdirectories. These will contain the main application logic and test files, respectively.
  • In both the main and test subdirectories, create a java subdirectory.
  • In the leap-year/src/main/java directory, create a LeapYear.java file to contain your back-end logic.
  • In the leap-year/src/test/java directory, create a LeapYearTest.java file. It will house the JUnit tests for your back-end logic.
  • In the top-level of the leap-year directory, create your build.gradle file.

Your directory structure should now look like this:

leap-year/
├── build.gradle
└── src/
    ├── main/
    │   └── java/
    │       └── LeapYear.java
    └── test/
        └── java/
            └── LeapYearTest.java

We can delay creating the App.java file. JUnit only tests back-end logic, not our user interface. Since App.java will contain our user interface, we can wait to create it.

build.gradle File

To configure Gradle, add the following to build.gradle:

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

archivesBaseName = "leap-year"
version = '1.0'
mainClassName = "LeapYear"

repositories {
  mavenCentral()
}

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

Class Files

Let's set up our LeapYear class file. We'll declare the classes and method, but will not include any logic within them. This is to avoid the compiler errors we saw in Testing with JUnit.

leap-year/src/main/java/LeapYear.java
public class LeapYear {

  public boolean isLeapYear(int year) {
    // eventually put your code here
    return false;
  }

}

Remember, the $ gradle test command first compiles source code, then runs tests on the compiled code. If a test includes classes or methods that aren't declared, the compiler will halt and throw an error. If the project does not compile successfully, the tests cannot run. Thus, we need to declare any classes or methods used in the test to avoid compiler errors. This allows us to run our tests, and confirm new tests are failing appropriately.

Since our isLeapYear() method will return a boolean, we add return false. This allows our application will compile properly.

Test Files

Next, let's set up our test file:

leap-year/src/test/java/LeapYearTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class LeapYearTest {

}

Here, we import the JUnit testing library: org.junit.*;, and JUnit's assertion library: static org.junit.Assert.*;. Then, we declare our LeapYearTest class, leaving it empty for now.

The Behavior-Driven Development Process

Now that everything is in place, we may begin the Behavior-Driven Development process. (Also known as the Red, Green, Refactor workflow).

Identify Simplest Behavior

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

This behavior should also remain applicable for the life of the program. Because we want to avoid changing previous specs as we build future specs.

In our case, the simplest possible behavior is determining whether a year is divisible by 4. The first criteria in checking whether a year is a leap year.

Writing a Coded Test

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

In our test file, we'll begin by declaring our test method:

leap-year/src/test/java/LeapYearTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class LeapYearTest {

  @Test
  public void isLeapYear_forNumberDivisibleByFour_true() {
    // test code will eventually go here.
  }

}
  • We add the required @Test annotation. This informs the compiler that JUnit will be responsible for running this code.
  • We declare the method as public void because all JUnit tests must be public void.
  • isLeapYear refers to the name of the method this spec will be testing.
  • forNumberDivisibleByFour is a brief description of the behavior this spec will test.
  • true is the output we are anticipating for the example input we provide momentarily.

Next, we'll add the content to our newly-declared test:

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

public class LeapYearTest {

  @Test
  public void isLeapYear_forNumberDivisibleByFour_true() {
    LeapYear leapYear = new LeapYear();
    assertEquals(true, leapYear.isLeapYear(2012));
   }

}
  • To test our isLeapYear() method we must create an instance of the LeapYear class.
  • The assertEquals() method instructs JUnit to compare its two arguments. Essentially, we're telling it to check whether leapYear.isLeapYear(2012) successfully returns true.

Make Sure the Test Fails

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

Run $ gradle test and see what happens. It should look something like this:

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

LeapYearTest > isLeapYear_forNumberDivisibleByFour_true FAILED
    java.lang.AssertionError at LeapYearTest.java:9

1 test 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/leap-year/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: 3.639 secs

Copy and paste your line that looks like: file:///Users/staff/Desktop/java/leap-year/build/reports/tests/test/index.html into your browser to view a detailed test report. Remember, you can refresh this page to see the current status of your tests every time you run $ gradle test.

first-failed-test-report

Implement the Behavior

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

We know we'll eventually need to check if the year is divisible by 100 or 400. But we only add code for our first behavior: Returning true when the year is divisible by 4.

leap-year/src/main/java/LeapYear.java
...
public boolean isLeapYear(int year) {
    return year % 4 == 0;
}
...

Run the Automated Test(s)

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

$ gradle test should return no error messages:

$ gradle test

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

BUILD SUCCESSFUL

Total time: 1.229 secs

And if we refresh our test report, we see the following:

first-passing-test

Make Sure Previous Tests Pass

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

Since this is currently our only test, we can skip this step.

Refactor

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

Since the logic we've added so far is very brief, we can't refactor quite yet, but you should always check.

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.

In our case, the next simplest behavior is returning false for years not divisible by four.

Write a Coded Test

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

Our next test will look like this:

LeapYearTest.java
…
@Test
  public void isLeapYear_forNumbersNotDivisibleByFour_false(){
    LeapYear leapYear = new LeapYear();
    assertEquals(false, leapYear.isLeapYear(1999));
  }
...

Again, we create an instance of the LeapYear class. Then, we use JUnit's assertEquals() method to confirm isLeapYear() correctly returns false when provided 1999.

Make Sure the Test Fails

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

Next, before we implement any new logic, we need to confirm this spec fails correctly.

We can run $ gradle test and see the following:

$ gradle test

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

BUILD SUCCESSFUL

Total time: 1.993 secs

If we refresh our test report in the browser, Click on Classes, then click on LeapYearTest we see this:

two-passing-tests-leap-year

It passed instead of failed!

If a test passes before you implement logic to make it pass, take a very careful look at your code. First, confirm the test itself is written correctly. Our test does look correct. There are no syntax errors. We included the correct @Test annotation. We instantiated the required instance of LeapYear. We provided the correct arguments to assertEquals(). It looks good.

If the test looks good, double-check your logic. Is there anything already present in this that would make this test pass? Right now, our isLeapYear() method looks like this:

leap-year/src/main/java/LeapYear.java
...
public boolean isLeapYear(int year) {
    return year % 4 == 0;
}
...

We're returning either true or false depending on whether year is evenly divisible by 4. 1999 from our last test isn't divisible by 4, so the method returned false.

Alright, we can confirm that this test isn't passing incorrectly. The logic we implemented to pass the previous test simply covers this behavior too. Sometimes this will happen. When it does, just be very careful in confirming the test is passing for the right reasons.

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.

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 still contained in a single, DRY line of code. There isn't much room for refactoring quite yet. We can move to the next step.

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.

Based on the leap year criteria, the next simplest behavior is returning false for numbers divisible by 100, since those years cannot be leap years.

Write a Coded Test

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

Let's add a spec for this behavior, too:

LeapYearTest.java
...
@Test
 public void isLeapYear_forMultiplesOfOneHundred_false() {
    LeapYear leapYear = new LeapYear();
    assertEquals(false, leapYear.isLeapYear(1900));
  }
...

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, all tests should pass except the most recent we've added:

$ gradle test

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

LeapYearTest > isLeapYear_forMultiplesOfOneHundred_false FAILED
    java.lang.AssertionError at LeapYearTest.java:21

3 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/leap-year/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.927 secs

third-failing-test-with-stacktrace

If we look at the stack trace from the test report it reads: java.lang.AssertionError: expected:<false> but was:<true>. This means that the test expected to receive false when running our isLeapYear() method with the sample input. But it actually received true instead. Since this was not the anticipated output, the test fails.

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. We can simply add to our existing conditional:

LeapYear.java
public boolean isLeapYear(int year) {
    if ( year % 100 == 0 ) {
      return false;
    } else {
      return year % 4 == 0;
    }
  }

Run the Automated Tests

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

Run the tests once more:

$ gradle test

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

BUILD SUCCESSFUL

Total time: 1.529 secs

three-passing-tests

Everything passes! Perfect!

Make Sure Previous Tests Pass

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

If we look at the report from the previous section, we can easily confirm all tests are still passing.

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 double-check if we can refactor. Again, since we've been adding the smallest amount of code possible when implementing each behavior, there isn't much room for refactoring.

Repeat

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

There is one last behavior our program will need to implement.

Identify a Behavior

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

Since any year divisible by 400 is a leap year, the application should return true for years divisible by 400.

Write a Coded Test

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

We'll add a test for this final behavior:

LeapYearTest.java
...
@Test
 public void isLeapYear_forMultiplesOfFourHundred_true() {
    LeapYear leapYear = new LeapYear();
    assertEquals(true, leapYear.isLeapYear(2000));
 }
...

Make Sure the Test Fails

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

We'll run our new test to ensure it fails correctly:

$ gradle test

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

LeapYearTest > isLeapYear_forMultiplesOfFourHundred_true FAILED
    java.lang.AssertionError at LeapYearTest.java:27

4 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/leap-year/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: 2.384 secs

fourth-failing-test

Implement the Behavior

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

And we'll implement the smallest amount of code to create this behavior:

LeapYear.java
public boolean isLeapYear(int year) {
    if ( year % 400 == 0 ) {
      return true;
    } else if ( year % 100 == 0 ) {
      return false;
    } else {
      return year % 4 == 0;
    }
  }

Run the Automated Tests

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

We'll run our tests again:

$ gradle test

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

BUILD SUCCESSFUL

Total time: 2.102 secs

four-passing-tests

Make Sure Previous Tests Pass

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

Great! Both our most current and all previous tests pass! We should have a completely green test report, and a fully functional isLeapYear() method.

Refactor

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

Because we've added so little logic for each behavior, our code is already DRY. But always double-check whether you can refactor. If you can, confirm all tests pass after making changes.

Front-End User Interface

Now that we've built DRY, well-tested back-end logic, we can add our user interface. Since the back-end code is well-tested, if any errors occur when we launch our program we'll know the bug resides in the user interface logic. After all, the back-end logic is already vetted. Doesn't that make tracking down bugs so much easier?

Create an App.java file in the leap-year/src/main/java directory.

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

leap-year/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 a prompt to ask users to enter a year:

leap-year/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 a year, we'll tell you if it's a leap year:")
  }
}

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

leap-year/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 a year, we'll tell you if it's a leap year:");
    String stringYear = myConsole.readLine();
    int intYear = Integer.parseInt(stringYear);
  }
}

Finally, we'll create an instance of the LeapYear class, call our isLeapYear() method, and print its results for the user:

leap-year/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 a year, we'll tell you if it's a leap year:");
    String stringYear = myConsole.readLine();
    int intYear = Integer.parseInt(stringYear);
    LeapYear leapYear = new LeapYear();
    boolean leapYearResult = leapYear.isLeapYear(intYear);
    System.out.println("Is that year a leap year?" + leapYearResult);
  }
}

We can use Gradle's $ 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

And we should see the following:

$ java App

Enter a year, we'll tell you if it's a leap year:

If we provide a year, it should tell us whether it is a leap year:

$ java App

Enter a year, we'll tell you if it's a leap year:
>2018

Is that year a leap year? false

Awesome! Continue to follow the "Red, Green, Refactor" workflow closely as you create Java applications. Using Behavior-Driven Development in this fashion is required for the rest of the course. Reference the Red, Green, Refactor lesson for as long/often as you need to. Eventually, it will become second nature!


Example GitHub Repo for Leap Year

Overview


  • This lesson does not address any new concepts, it is simply another demonstration of what creating a Java application using Behavior-Driven Development and the "Red, Green, Refactor" workflow looks like. For cheatsheets on any of the concepts covered here, visit the lessons these concepts were introduced in:

Example


Example GitHub Repo for Leap Year