Lesson Monday

In this lesson we'll continue to build the Ping Pong application using "Red, Green, Refactor" workflow and JUnit tests.

You are not required to code along with this lesson when completing it as homework. Instead, simply read along and familiarize yourself with the concepts. You'll follow along to code the application from this lesson with your partner tomorrow.

To internalize the Red, Green, Refactor workflow, it's recommended to keep this lesson open while following along.

Red, Green, Refactor with JUnit

Identify the Simplest Behavior

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

In the previous lesson we identified our program's simplest behavior as counting to 1. We chose this behavior because the first thing a ping pong app must do is to count to the provided number. This functionality must be present before replacing numbers with "ping" or "pong".

Write a Coded Test

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

We already wrote our first JUnit test in the previous lesson:

src/test/java/PingPongTest.java
import org.junit.*;
import static org.junit.Assert.*;
import java.util.List;
import java.util.ArrayList;

public class PingPongTest {

  @Test
  public void runPingPong_countUpToOne_ArrayList() {
    PingPong testPingPong = new PingPong();
    List<Object> expectedOutput = new ArrayList<Object>();
    expectedOutput.add(1);
    assertEquals(expectedOutput, testPingPong.runPingPong(1));
  }

}

Make Sure the Test Fails

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

After troubleshooting our compiler errors in the last lesson, we were able to receive an (appropriately) failing first test:

intellij-assertion-error

Implement the Behavior

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

Let's pick up right where we left off! We've identified our first behavior, written a coded JUnit test, and made sure it fails. Now let's add the smallest amount of logic to pass our first spec.

In our case, we need to return an ArrayList counting to 1:

src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){
    ArrayList<Object> result = new ArrayList<Object>();
    result.add(1);
    return result;
  }

}

Here, we use add() to insert 1 into our ArrayList. But we know we'll eventually need multiple numbers in this ArrayList. After all, what if the user provides the number 2? We would need to insert 1 and 2.

Even though it may seem silly, we only want to add the bare minimum amount of code to make this single spec pass. We cannot get ahead of ourselves by implementing additional behaviors. Developing this habit while our programs are still small will prove a huge benefit when their complexity increases.

Run the Automated Test

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

We can run our test again either by right-clicking on the file header tab of PingPongTest, or choosing to Run the test again by clicking on the play window in the bottom left area of the screen.

It passed! Those little green dots are so soothing.

Make Sure Previous Tests Pass

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

Since we currently have only one test, we'll skip this step this time.

Refactor

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

Our code is already very DRY. There's no room for refactoring, but it's important to check.

Repeat

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

We just completed the entire Red, Green, Refactor workflow with JUnit for the first time. Nice work! We'll repeat this process for each additional behavior until the application is complete.

Identify a Behavior

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

What's the next simplest behavior? We already know our program can count to one. Let's confirm it can count to 2!

Know that we're not choosing 2 because it comes after 1. If we chose a number like 3 and expected the program to return [1,2,3] that test would pass at first. But, it would later fail when we began replacing numbers divisible by 3 with "ping". Then, we'd have to go backwards and alter this test to make it pass again. We don't want to do that. In fact, we want to avoid modifying past specs as much as possible. Try to write specs that will remain true for the life of the program.

We chose 2 because runPingPong(2) should always return [1,2], even after the application is complete; even after we add logic to replace numbers divisible by 3 with "ping", this test will still pass; when we add logic to replace numbers divisible by 5 with "pong" this test will pass; and when we add logic to replace numbers divisible by 5 and 3 with "ping pong" this test will still pass. That's what it means when we say a test behavior should "remain true for the life of the program".

It's okay if this concept is confusing at first. As you use BDD more and more, the importance of this will become clearer.

Write a Coded Test

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

Alright, instead of manually typing our test syntax, IntelliJ can actually generate a skeleton test for us.

Right-click underneath our existing test (but still inside the PingPongTest class) and select Generate > Test Method.

We'll see that IntelliJ generates boilerplate code for us. The red box around name indicates that we can immediately edit the name. Ignore the throws Exception part, and make your test match the example below:

src/test/java/PingPongTest.java
import org.junit.*;
import static org.junit.Assert.*;
import java.util.List;
import java.util.ArrayList;

public class PingPongTest {

  @Test
  public void runPingPong_countUpToOne_ArrayList() {
    PingPong testPingPong = new PingPong();
    ArrayList<Object> expectedOutput = new ArrayList<Object>();
    expectedOutput.add(1);
    assertEquals(expectedOutput, testPingPong.runPingPong(1));
  }

  @Test
  public void runPingPong_countUpToGivenNumber_ArrayList() throws Exception {
   PingPong testPingPong = new PingPong();
   ArrayList<Object> expectedOutput = new ArrayList<Object>();
   expectedOutput.add(1);
   expectedOutput.add(2);
   assertEquals(expectedOutput, testPingPong.runPingPong(2));
 }
}

As you can see, this spec looks similar to the first.

  • It is preceded with the @Test annotation.
  • It is a public void method.
  • The method name uses the nameOfMethodWeAreTesting_descriptionOfBehavior_returnValue convention.
  • Because the method returns an ArrayList, we build an ArrayList within the test to compare the return value of runPingPong() to.
  • We use JUnit's assertEquals() method to compare our expectedOutput to the result of runPingPong(2).

Also, note that the variable name expectedOutput is not significant to JUnit. Like any other variable, we could theoretically call it anything we'd like.

Note on "Throws Exception"

The phrase "throws Exception" is related to the manner Java handles errors. Because Java is so concerned with stability, we are required to tell Java how to behave if it encounters an error.

We will discuss exceptions in detail soon, but for now, simply ignore this phrase. You may see some tests with "throws Exception" in place, and others without. Nowadays it's considered better practice to include it than to omit it, but you may still see some tests omit it, as this was an older standard.

Make Sure the Test Fails

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

Moving on, let's make sure our new test fails before implementing logic. Run your tests again, and we should see on our screen that our first test still passes, but our second test errored with the following assertion error:

java.lang.AssertionError:
Expected :[1, 2]
Actual   :[1]
 
    at org.junit.Assert.fail(Assert.java:88)
    at org.junit.Assert.failNotEquals(Assert.java:834)
    at org.junit.Assert.assertEquals(Assert.java:118)
    at org.junit.Assert.assertEquals(Assert.java:144)
    at models.PingPongTest.runPingPong_countUpToGivenNumber_ArrayList(PingPongTest.java:24)

Again, this is what we want - It's a big problem if a test passes without us writing the code to make it pass!

Implement the Behavior

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

Now that we've written another valid test, we can implement the least amount of logic to create the behavior it depicts.

To return an ArrayList counting to the provided number we'll use a loop:

src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){
    ArrayList<Object> result = new ArrayList<Object>();
    for (int i = 1; i <= countUpTo; i++){
      result.add(i);
    }
    return result;
  }

}

Run the Automated Test

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

Notice we've only added logic to runPingPong() for the singular behavior we've identified. Let's see if it works! Run your tests again, and you should see two passing tests. Cool. We're building one step at a time, checking our process as we go.

Make Sure Previous Tests Pass

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

As we can see, all tests are passing. Rad!

Refactor

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

Even though we've added more code, we still don't have room to refactor. This is alright, and will likely happen often. Especially with these smaller programs. But never skip this step, always take a moment to check.

Repeat

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

We've followed the Red, Green, Refactor workflow for two behaviors now! Are you beginning to get the hang of it? Let's address the next behavior.

Identify a Behavior

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

We've confirmed our program can successfully count to a provided number. Now, let's begin replacing certain numbers with strings. The next simplest behavior our program will need to demonstrate is replacing numbers divisible by 3 with "ping".

Notice our behavior isn't something like "The program replaces all eligible numbers with strings". Identifying and replacing multiples of 3 is a different action than identifying and replacing multiples of 5. It requires a distinctly different line of code. They're considered two different behaviors that require two different tests.

Write a Coded Test

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

Now, let's write the coded test to assert that our application can successfully replace numbers divisible by 3 with "ping":

src/test/java/PingPongTest.java
...
  ...
  @Test
  public void runPingPong_replaceMultiplesOf3_ArrayList() {
    PingPong testPingPong = new PingPong();
    ArrayList<Object> expectedOutput = new ArrayList<Object>();
    expectedOutput.add(1);
    expectedOutput.add(2);
    expectedOutput.add("ping");
    assertEquals(expectedOutput, testPingPong.runPingPong(3));
  }
...

Again, we include the @Test annotation and declare a public void method. The method name follows the naming conventions we've used previously: runPingPong is the name of the method this spec is testing. replaceMultiplesOf3 is a brief description of the behavior we're testing, and ArrayList is what the method should return.

Make Sure the Test Fails

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

As always, we need to confirm this new test fails before adding logic. Run your test, and we should receive the following results:

java.lang.AssertionError:
Expected :[1, 2, ping]
Actual   :[1, 2, 3]
 

Implement the Behavior

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

We've written another test that (correctly) fails. Now, let's implement this behavior:

src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){
    ArrayList<Object> result = new ArrayList<Object>();
    for (int i = 1; i <= countUpTo; i++){
      if (i % 3 == 0){
        result.add("ping");
      } else {
        result.add(i);
      }
    }
    return result;
  }

}

Here, we add a conditional that checks if the current i is evenly divisible by 3. If so, we add "ping" to result. If not, we add i.

Run the Automated Test

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

Once more, we'll run our tests to confirm this behavior is functional - we should see a passing test. Our screen should currently look like this:

intellij-three-passing-tests

Make Sure Previous Tests Pass

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

As we can see above, all specs currently pass. Great work!

Refactor

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

Again, since we've focused on adding the bare minimum amount of code for each behavior, we don't have room to refactor.

Repeat

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

Three behaviors down! Let's move on to the next.

Identify a Behavior

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

In addition to replacing all numbers divisible by 3 with "ping", we also have to replace those divisible by 5 with "pong". Let's address this behavior next.

Write a Coded Test

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

Again, we'll write a coded test:

src/test/java/PingPongTest.java
...
  ...

  @Test
  public void runPingPong_replaceMultiplesOf5_ArrayList() {
    PingPong testPingPong = new PingPong();
    ArrayList<Object> expectedOutput = new ArrayList<Object>();
    expectedOutput.add(1);
    expectedOutput.add(2);
    expectedOutput.add("ping");
    expectedOutput.add(4);
    expectedOutput.add("pong");
    assertEquals(expectedOutput, testPingPong.runPingPong(5));
  }
...

Make Sure the Test Fails

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

Once more, we'll run our tests to ensure the new test fails correctly:

java.lang.AssertionError:
Expected :[1, 2, ping, 4, pong]
Actual   :[1, 2, ping, 4, 5]
 

Implement the Behavior

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

And, after confirming the test fails appropriately, we'll implement this behavior:

src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){
    ArrayList<Object> result = new ArrayList<Object>();
    for (int i = 1; i <= countUpTo; i++){
      if (i % 3 == 0){
        result.add("ping");
      } else if (i % 5 == 0){
        result.add("pong");
      } else {
        result.add(i);
      }
    }
    return result;
  }

}

Run the Automated Test

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

After adding logic to exhibit the behavior, we'll run our tests again to see if it works. And it does! 4 green dots.

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!

Refactor

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

Again, not much room for refactoring. But as our programs grow in complexity, you'll find more opportunities to refactor when reaching this step.

Repeat

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

There's still one more behavior our ping pong application requires. Let's complete each step one more time.

Identify a Behavior

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

As you know, our application also needs to replace numbers divisible by both 5 and 3 with "pingpong".

Write a Coded Test

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

We'll write another coded test:

src/test/java/PingPongTest.java
...
  ...

  @Test
  public void runPingPong_replaceMultiplesOfBoth3And5_ArrayList() {
    PingPong testPingPong = new PingPong();
    ArrayList<Object> expectedOutput = new ArrayList<Object>();
    expectedOutput.add(1);
    expectedOutput.add(2);
    expectedOutput.add("ping");
    expectedOutput.add(4);
    expectedOutput.add("pong");
    expectedOutput.add("ping");
    expectedOutput.add(7);
    expectedOutput.add(8);
    expectedOutput.add("ping");
    expectedOutput.add("pong");
    expectedOutput.add(11);
    expectedOutput.add("ping");
    expectedOutput.add(13);
    expectedOutput.add(14);
    expectedOutput.add("pingpong");
    assertEquals(expectedOutput, testPingPong.runPingPong(15));
  }
...

Make Sure the Test Fails

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

We'll run our tests as previously to ensure this new test fails appropriately:

java.lang.AssertionError:
Expected :[1, 2, ping, 4, pong, ping, 7, 8, ping, pong, 11, ping, 13, 14, pingpong]
Actual   :[1, 2, ping, 4, pong, ping, 7, 8, ping, pong, 11, ping, 13, 14, ping]
 

Implement the Behavior

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

Next, we'll implement this behavior in our back-end logic:

src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){
    ArrayList<Object> result = new ArrayList<Object>();
    for (int i = 1; i <= countUpTo; i++){
      if (i % 3 == 0 && i % 5 == 0){
        result.add("pingpong");
      } else if (i % 3 == 0){
        result.add("ping");
      } else if (i % 5 == 0){
        result.add("pong");
      } else {
        result.add(i);
      }
    }
    return result;
  }

}

Run the Automated Test

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

Next, we'll run our tests again - and we should see 5 green dots.

Make Sure Previous Tests Pass

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

Great! All tests pass!

Refactor

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

Again, not much room for refactoring. But always take a moment to double-check.

Complete!

Our back-end logic is now complete! We've identified, implemented, and tested each behavior a ping pong application must exhibit. Nice work! Hopefully it's becoming clearer how to use this workflow in your own projects.

Frontend User Interface

Thanks to our careful testing, we know all back-end logic is functional. Let's add a user interface to run our program in the command line.

Remember, JUnit tests are for backend logic only. We don't write JUnit tests for anything our user interface does. We also want to wait to create our user interface until we know all back-end code has been thoroughly tested.

We should already have an App.java file. Make it match the below:

src/main/java/App.java
public class App {
  public static void main(String[] args) {

    System.out.println("I'm a ping-pong application!");

  }
}

Next, let's add code to interact with users, and call our back-end logic. We'll use our BufferedReader again for this.

src/main/java/App.java
import models.PingPong;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;

public class App {
   public static void main(String[] args) {
       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
       System.out.println("I'm a ping-pong application!");
       System.out.println("Enter a number:");
       try {
           String stringUserNumber = bufferedReader.readLine();
           int intUserNumber = Integer.parseInt(stringUserNumber);
           PingPong pingPong = new PingPong();
           ArrayList<Object> pingPongResult = pingPong.runPingPong(intUserNumber);
           System.out.println(pingPongResult);
       }
       catch (IOException e){
           e.printStackTrace();
       }
   }
}

  • We prompt the user to enter a number.
  • We gather user input with readLine().
  • We parse user input (a String by default) into an Integer.
  • We create an instance of the PingPong class.
  • We call runPingPong(), passing in the user's number.
  • Results are printed for the user.

Let's compile and run our app, and finally, we can interact with our application's command line interface:

I'm a ping-pong application!
Enter a number:
24
[1, 2, ping, 4, pong, ping, 7, 8, ping, pong, 11, ping, 13, 14, pingpong, 16, 17, ping, 19, pong, ping, 22, 23, ping]


Example GitHub Repo for Ping Pong wtih BDD and Frontend