Test-Driven Development Demonstration

2026-09-06

Introduction

This is a tutorial in Test Driven Development(TDD) in Java using `Leetcode’s challenge “Best Time to Buy and Sell Stock III” 1. It covers some basic Java setup and a work-through of an interview-style problem.

Kent Beck's Book Test-Driven Development By Example provides an informal three-part mantra.2

  1. Red --- Write a little test that doesn't work, and perhaps doesn't even compile at first
  2. Green --- Make the test work quickly, committing whatever sins necessary in the process.
  3. Refactor --- Eliminate all of the duplication created in merely getting the test to work.

The right way to write software is the way that works. The way that works depends on the context. Many companies’ production methods are test-driven. A programmer at such a shop does nobody any favors by vibecoding unit tests after the fact. It helps to learn learn the organization’s house style.

There are no rules, just tools.

--- Glen Vipplu

Setup

Dependencies and Assumptions

Software

Leetcode’s interpreter uses Java version 25, so the programmer needs version 25 or higher.3.

To create the project and run its tests, the programmer also needs Apache Maven. Maven is a "build tool for Java projects"4 that automates the boilerplate. That includes the project’s directory structure, file names, class names and test scaffholding. Yes, but can be written by hand, but Java is so verbose that and we want to reduce manual configuration.

Skills

In addition to basic Computer Science concepts (like array indexing) and jargon, the programmer should already familiar with their operating system's package manager and terminal. Everybody's package manager is different, so we're not covering how to install and update software. It's assumed the programmer has a favorite (brew, apt etc) and knows how to use it. I wrote this tutorial on a Mac, so Windows or Powershell users may need to adapt their own commands as necessary.

First, confirm Java and Maven(mvn) are installed.

java --version
mvn --version

If either --version call returns command not found, then they need to be installed(and that's out of scope here).

Creating the Project

Generating from the Maven Archetype

To create a project from a template, use the command mvn archetype:generate.“Archetype” is the Maven term for a project template. The default template is maven-archetype-quickstart. Even when using defaults, values are still needed for group id, artifact id, package and version.

Here’s what I used for this tutorial.

fieldvalue
group idcom.programming_grimoire
artifact idtdd_demo
packagecom.programming-grimoire.buy_sell_stock_iii
version1.0.0

mvn archetype:generate runs interactively by default. It can also be run in batch mode (the -B flag) to pass in all the parameters at once.

mvn archetype:generate -B -DgroupId=com.programming_grimoire -DartifactId=tdd_demo -Dversion=1.0.0. -Dpackage=com.programming_grimoire.buy_sell_stock_iii

NOTE: For the interest of sanity, console output here is truncated.

...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

Maven built a project complete with a pom.xml file, starter app and test.

tdd_demo
|-- pom.xml
|-- src
   |-- main
   |   |-- java
   |       |-- com
   |           |-- programming_grimoire
   |               |-- buy_sell_stock_iii
   |                   |-- App.java
   |-- test
       |-- java
           |-- com
               |-- programming_grimoire
                   |-- buy_sell_stock_iii
                       |-- AppTest.java

12 directories, 3 files

Hardening the POM

The P.O.M. in pom.xml stands for Project Object Model, and the file is Maven's representation of the project. The generated POM is functional but minimal; building with it as-is throws out [WARNING] leaders in the output.

Warnings don't break the build, but they:

  1. remain harmless until the worst possible moment
  2. add noise to the outputs
  3. annoy the programmer

For text, my machine uses UTF-8 encodings as a default. Use an xml tag to spell it out for Maven.

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

In this example, Maven uses Java 26. If the Java version isn’t secured, the project may compile using an earlier release.

<maven.compiler.release>26</maven.compiler.release>

The source encoding and version specifications go in the POM's properties field.

<properties>
 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 <maven.compiler.release>26</maven.compiler.release>
</properties>

It goes in the top-level project tag so the final POM looks like the example below.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.programming_grimoire</groupId>
  <artifactId>tdd_demo</artifactId>
  <packaging>jar</packaging>
  <version>1.0.0.</version>
  <name>tdd_demo</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <maven.compiler.release>26</maven.compiler.release>
  </properties>
</project>

Running the Tests

Let's go over running tests, project cleaning and introducing some shorthand to cut the blocks of output in this tutorial. Running the tests is simple; use mvn test from inside the project directory.

Basic Case

The content of AppTest has one boilerplate test named testApp.

public void testApp()
   {
       assertTrue( true );
   }

Initial Run

Testing is easy; run mvn test from the project root(tdd_demo).

mvn test

The output should include the following.

...
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s -- in com.programming_grimoire.buy_sell_stock_iii.AppTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
...

Testing is also a build process. It generates the target directory, and with that, a ton more files.

When the project was generated:

12 directories, 3 files

After mvn test:

32 directories, 11 files

We want to clean the project before the next test.

mvn clean

After mvn clean:

32 directories, 11 files

So running the tests is a two-motion sequence. Don’t worry; it becomes reflexive.

mvn clean
mvn test

The Problem Statement

The Following comes from Leetcode Problem 123: ''Best Time to Buy and Sell Stock III'' 1

You are given an array prices where prices[i] is the price of a given stock on the i^th day. Find the maximum profit you can achieve. You may complete at most two transactions. Note: You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).

The description includes some information we'll use in the tutorial.

We're given Example 1 with an expected input, output, and an explanation.

Input: prices = [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3. Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.

The description also promises the following constraints.

1 <= prices.length <= 10^5
0 <= prices[i] <= 10^5

Leetcode provides the following stub for Java users.

class Solution {
   public int maxProfit(int[] prices) {

   }
}

All of this information is used in the tutorial.

The TDD Cycle

Minimum Test Case

Red

We'll need to construct some scaffolding before writing the first test.

Since the class name in Leetcode’s stub is Solution, there should be a SolutionTest.

touch src/test/java/com/programming_grimoire/buy_sell_stock_iii/SolutionTest.java

SolutionTest should be a part of the package buy_sell_stock_iii.

package com.programming_grimoire.buy_sell_stock_iii;

SolutionTest also requires jUnit imports or to actually, you know, run the tests.

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

Per Java convention, the class name is SolutionTest just like the file name.

public class SolutionTest extends TestCase {

The same is true for the constructor.

public SolutionTest( String testName )
{
    super( testName );
}

The class for SolutionTest should be passed to the convenience constructor, so test cases will be added automatically added to the suite. 5

public static Test suite()
  {
      return new TestSuite( SolutionTest.class );
  }

Using the convenience constructor forgoes the need to manually add test names to the suite.

public static Test suite() {
   TestSuite suite= new TestSuite();
   suite.addTest(new SolutionTest("testMinimumInput"));
   suite.addTest(new SolutionTest("testArrayOfTwo"));
   ...

Once the initial scaffolding is set up, the minimal failing test can be written and run.

According to the description, an input array may be of length of one.

1 <= prices.length <= 10^5

A profit isn't possible in this case. An investor may buy, but is unable to sell.

So for the sequence of 0, the expected answer is 0

int[] prices = new int[]{0};
int expectedResult = 0;

The full test case:

public void testMimimumInput()
     {
         int[] prices = new int[]{0};
         int expectedResult = 0;

         Solution s = new Solution();
         int actual = s.maxProfit(prices);

         assertEquals(expectedResult, actual);
     }

Clean the project and run the test suite to confirm the new test fails as expected. No implementation code has been written yet.

mvn clean
mvn test
...
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] .../tdd_demo/src/test/java/com/programming_grimoire/buy_sell_stock_iii/SolutionTest.java:[24,8] cannot find symbol
 symbol:   class Solution
 location: class com.programming_grimoire.buy_sell_stock_iii.SolutionTest
...

Green

This is the point to commit every sin imaginable; to rush ahead and implement, a Solution class that has a maxProfit instance method. T

The compilation error occurred because "Solution" doesn't exist in the project, so Solution needs to be created.

touch src/main/java/com/programming_grimoire/buy_sell_stock_iii/Solution.java

Like SolutionTest, Solution should be part of the buy_sell_stock_iii package.

package com.programming_grimoire.buy_sell_stock_iii;

...and export the proper class name.

class Solution {
#+end_example****

"Solution.java" also needs to have that maxProfit function which takes an integer array as an argument.

#+begin_example java
public int maxProfit(int[] prices) {
 return 0;
 }

The tests should run green now.

mvn clean
mvn test
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.programming_grimoire.buy_sell_stock_iii.AppTest
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 s -- in com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
...

There's no compilation errors nor test failures; the tests ran green.

Refactor

One test was written, but two tests ran.

[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

There’s now unnecessary work in the project.

tdd_demo
|---pom.xml
|---src
   |-- main
   |   |-- java
   |       |-- com
   |  |-- programming_grimoire
   |               |-- buy_sell_stock_iii
   |                   |-- App.java
   |                   |-- Solution.java
   |-- test
       |-- java
           |-- com
               |-- programming_grimoire
                   |-- buy_sell_stock_iii
                       |-- AppTest.java
                       |-- SolutionTest.java

12 directories, 6 files
#+end_example***

Out damned spots.

#+begin_src bash
 rm src/main/java/com/programming_grimoire/buy_sell_stock_iii/App.java
 rm src/test/java/com/programming_grimoire/buy_sell_stock_iii/AppTest.java
#+end_src

It seems trivial, but verify the tests still pass.
Unit tests like this need to be cheap to run in order to facilitate the “red, green, refactor” rhythm.

#+begin_example bash
mvn clean
mvn test
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.programming_grimoire.buy_sell_stock_iii.SolutionTest

Cool! Now break it again.

Array of Length Two

Red

The answer for all sequences of length one is the same, so let’s expand the input size a little. It takes two days to make a transaction. In the case of a sequence 5,7, The investor would buy 5, then sell 7. The expected answer is 2.

int[] prices = new int[]{5, 7};
int expectedResult = 2;

The test case:

public void testArrayOfLengthTwo(){
     <<case-of-two>>

      Solution s = new Solution();
      int actual = s.maxProfit(prices);

    assertEquals(expectedResult, actual);
  }

The existing implementation is a hard coded return 0;, so the above test should fail.

...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.012 s <<< FAILURE! -- in com.programming_grimoire.buy_sell_stock_iii.SolutionTest
...
[ERROR] Failures:
[ERROR]   com.programming_grimoire.buy_sell_stock_iii.SolutionTest#testInputOfTwo AssertionFailedError expected:<2> but was:<0>
...

expected<2> but was:<0>: those are the droids we were looking for.

Green

Determining return values based on array length is a fast way to pass the existing tests. If the sequence is one day long, the investor can’t sell, and no transaction is possible.

if (prices.length == 1){
      return 0;

Otherwise, the investor can buy on the first day and sell on the second.

}else{
    return prices[1] - prices[0];
}

In the current universe of two tests, that implementation is correct.

...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
...

Refactor

There's now duplication in the test suite.

 public void testMimimumInput()
 {
      int[] prices = new int[]{0};
      int expectedResult = 0;

      Solution s = new Solution();
      int actual = s.maxProfit(prices);

      assertEquals(expectedResult, actual);
 }

 public void testArrayOfLengthTwo()
 {
    int[] prices = new int[]{5, 7};
    int expectedResult = 2;

    Solution s = new Solution();
    int actual = s.maxProfit(prices);

    assertEquals(expectedResult, actual);
}

Specifically, there's a repeating pattern in instantiating a class and calling maxProfit every time to test an expected input and output..

Solution s = new Solution();
int actual = s.maxProfit(prices);

assertEquals(expectedResult, actual);

That functionality can be extracted to it's own method.

private void assertMaxProfit(int[] prices, int expected){
     Solution s = new Solution();
     int actual = s.maxProfit(prices);
     assertEquals(expected, actual);
}

That compacts each test case.

public void testMimimumInput()
   {
        int[] prices = new int[]{0};
        int expected = 0;
        assertMaxProfit(prices, expected);
   }

In turn, the test document reads a little cleaner.

 package com.programming_grimoire.buy_sell_stock_iii;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

public class SolutionTest extends TestCase {

  public SolutionTest( String testName )
  {
    super( testName );
  }

  public static Test suite()
  {
       return new TestSuite( SolutionTest.class );
  }

  public void testMimimumInput()
  {
       int[] prices = new int[]{0};
       int expected = 0;
       assertMaxProfit(prices, expected);
  }

  public void testArrayOfLengthTwo()
  {
     int[] prices = new int[]{5, 7};
     int expected = 2;
     assertMaxProfit(prices, expected);
  }
   private void assertMaxProfit(int[] prices, int expected){
        Solution s = new Solution();
        int actual = s.maxProfit(prices);
        assertEquals(expected, actual);
   }
}

Test to confirm the refactor of the test doesn't change anything.

mvn clean
mvn test
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
...

Think of it like carrying an umbrella. It always seems to rain when you don’t have the umbrella with you.

Array of Length Two, Non-Increasing

Red

;;; This is a functional code block, don't touch. ;; Resume live text.

What if prices don't go up? For the sequence 9,5, the answer would be 0 because the investor would not walk away having lost 4 of whatever it is they're buying and selling.

public void testArrayOfLengthTwoNoProfit()
{
    int[] prices = new int[]{9, 5};
    int expected = 0;
    assertMaxProfit(prices, expected);
}

Does it break as expected?

...
[ERROR] com.programming_grimoire.buy_sell_stock_iii.SolutionTest.testArrayOfLengthTwoNoProfit -- Time elapsed: 0 s <<< FAILURE!
junit.framework.AssertionFailedError: expected:<0> but was:<-4>
...

Yes. The current implementation returns -4 instead of 0.

Green

The logic can be updated: if the sequence length is two and the second price is larger, then return the difference.

if (prices.length == 2 && prices[1] > prices[0]){
    return prices[1] - prices[0];

For all other cases, the answer is 0.

else{
    return 0;
}

Confirm it passes.

...
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
...

Refactor

There's duplication in the code; prices[0] and prices[1] are indexed twice.

The problem can be rephrased. The first and second elements are also the first and last elements.

int firstIndex = 0;
int lastIndex = prices.length-1;

We still obtain the difference.

int difference = prices[lastIndex] - prices[firstIndex];

In a sequence of one difference is 0 because it’s one element minus itself. If the difference is negative, no profit is possible. Otherwise the difference is the answer.

return difference < 0 ? 0 : difference;

That sounds flashy. Let's check to confirm it didn't break anything.

...
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
...

Array of Size Three

Red

The solution is perfectly fine as long as the optimum profit is gained by selling on the last day. However, the sequence 1, 3, 2 should have a profit of 2, not 1. The investor would know that they should sell early.

public void testArrayOfLengthThree()
{
    int[] prices = new int[]{1,3,2};
    int expected = 2;
    assertMaxProfit(prices, expected);
}

Smash it up.

...
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR]   com.programming_grimoire.buy_sell_stock_iii.SolutionTest#testArrayOfLengthThree AssertionFailedError expected:<2> but was:<1>
...

Green

At this state, the investor may investigate every possible buy and sell date, and track which profit from those potential transactions is the largest. Before any work's done, that maximum profit is 0.

int maxProfit = 0;

Every possible combination can be investigated by cycling through the possible indices i (for a possible purchase) and j (for a possible sale), such that j is in bounds for prices and i is less than j.

for (int j=1; j< prices.length; j++){
    for (int i=0; i< j; i++){

At this point, the programmer isn't optimizing, just committing every sin imaginable to get to green. As before, a possible profit is identified by calculating a difference.

int difference = prices[j] - prices[i];

If the difference is more than the maximum profit, then a new maximum profit has been determined.

if (difference > maxProfit){
    maxProfit = difference;
    }

 }
}

The answer is the end value of the maximum profit.

return maxProfit;

It's faster to write than describe.

public int maxProfit(int[] prices) {
   int maxProfit = 0;
   for (int j= 1; j < prices.length; j++){
       for (int i = 0; i < j; i++){
	    int difference = prices[j] - prices[i];
	    if (difference > maxProfit){
		maxProfit = difference;
           }
       }
   }
   return maxProfit;
 }

Even though it looks like it should work, confirm that it does.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.522 s
[INFO] Finished at: 2026-09-03T22:23:28-05:00
[INFO] ------------------------------------------------------------------------

Refactor

The nested loop yields a time complexity of O(n^2).That’s gonna barf up a brick once Leetcode scales up the input sizes.

Profit can be decomposed into terms of gain and loss.

\begin{equation} profit=gain-loss \end{equation}

These are now the factors of interest to the investor at each point in the traversal. At the first element of the sequence, were the investor to sell now they'd earn nothing because they have nothing. So the initial gain is 0.

int gain = 0;

Were the investor to buy the first element in the sequence they'd just lose that amount of money.So the initial loss is the first price in the sequence.

int loss = prices[0];

With that in mind, the investor checks each additional term in the sequence after the first to see if they can get a better deal.

for (int i = 1; i < prices.length; i++) {
  int currentPrice = prices[i];

At each price, the investor asks two questions: Will buying at this price lower my loss? and Will selling at this price maximize my gain?.

If buying now lowers the loss, then this is current best buying price.

if (currentPrice < loss ){
  loss = currentPrice;
}

If selling now raises the gain, then this is current best selling price.

int net = currentPrice - loss;
if (net > gain){
  gain = net;
}

After traversal has been completed, the investor has determined their maximum possible gain.

}
return gain;

All together, the implementation is just one flat loop, O(n).

int loss = prices[0];
for (int i = 1; i < prices.length; i++) {
  int currentPrice = prices[i];
if (currentPrice < loss ){
  loss = currentPrice;
}
int net = currentPrice - loss;
if (net > gain){
  gain = net;
}
}
return gain;

You know the drill.

... [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.programming_grimoire.buy_sell_stock_iii.SolutionTest [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.programming_grimoire.buy_sell_stock_iii.SolutionTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 ... #+end_example

Two Transactions

Red

The Example from Leetcode’s problem description outlines a case with two transactions. The sequence 3,3,5,0,0,3,1,4 should give the answer 6.

public void testTwoTransactions()
{
    int[] prices = new int[]{3,3,5,0,0,3,1,4};
    int expected = 6;
    assertMaxProfit(prices, expected);
}

So far, we've only one transaction as been tracked. The description mentions a sum of up to two transactions, so the existing implementation should fail.

... [INFO] Results: [INFO] [ERROR] Failures: [ERROR] com.programming_grimoire.buy_sell_stock_iii.SolutionTest#testExampleOne AssertionFailedError expected:<6> but was:<4> [INFO] [ERROR] Tests run: 5, Failures: 1, Errors: 0, Skipped: 0 ... #+end_example

Green

For the sequence 3,3,5,0,0,3,1,4, the investor determined the first 0 was the best purchase and that the 4 was the best sale. That's perfectly reasonable if only one transaction is allowed.

pricelossgain
330
330
532
002
002
303
103
404

It’s time to track two transactions.

The first transaction should look familiar. Like before, it’s tracking the potential gain and loss starting at the first item in the sequence.

int firstLoss=prices[0];
int firstGain=0;

The same is done for the second transaction.

int secondLoss = prices[0];
int secondGain = 0;

In the case for the non-increasing array, it didn't matter if the first index and the same index were the same item. If there's only one transaction, then the first transaction is the second transaction.

Initially, the solution looks similar to the one-transaction version. There's a traversal.

for (int i = 1; i < prices.length; i++) {
  int price = prices[i];

The logic for the first transaction is the same as before.

if (price < firstLoss ){
  firstLoss = price;
}

int firstNet = price - firstLoss;

if (firstNet > firstGain){
  firstGain = firstNet;
}

However the loss for the second transaction is offset by the highest possible gain from the first.

int offsetLoss = price - firstGain;

After that, the logic for the second transaction also updates and continues.

if (offsetLoss < secondLoss){
  secondLoss = offsetLoss;
}

int secondNet = price - secondLoss;

if (secondNet > secondGain){
   secondGain = secondNet;
}

At each price, the investor tracks the state of two potential gains and two potential losses. Note that the second loss is directly affected by the first gain.

pricefirst lossfirst gainsecond losssecond gain
33030
33030
53232
002-22
002-22
303-25
103-25
404-26

The final profit from the second traction is the answer.

}
return secondGain;

This iteration in full:

It sounds fancy, but does it work?

...
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
...

Refactor

Once again there’s duplication. Those =gain=s and =loss=es are proliferating like rabbits.

I’ve referred to gain/loss pairs as /transaction/s, so conceptually, they already belong to an abstract data type. That verbal encoding can be formalized as a Transaction class.

class Transaction {
    int gain;
    int loss;

Duplicated logic can then be encapsulated as instance methods. For instance, both transactions initialize with the same values for gain and loss.

int gain = 0;
int loss = prices[0];

That can be soaked up by the constructor.

public Transaction(int initialPrice){
    this.gain = 0;
    this.loss = initialPrice;
}

Both transactions update their loss the same way.

if (price < loss ){
  loss  = price;
}

That can be a mutator.

public void optimizeLoss(int price){
     if (price < loss ){
       this.loss = price;
     }
}

The pattern for updating gains can also be encapuslated.

int net = price - loss;
     if (net > gain){
       gain = net;
     }

For lack of a better sounding signature, I’m going with optimizeLoss and optimizeGain.

public void optimizeGain(int price){
    int net = price - this.loss;
    if (net > this.gain){
        this.gain = net;
    }
}

Ordinarily, we'd go with one class per file, however there are a couple of factors that make this different. Leetcode’s environment is a one-document deal. Nobody wants to pull from multiple files to copy+pase a solution into a browser field. Following the established pattern, the programmer wants to motivate the need for a .java file by way of a failing test suite. Since Transaction class is subordinate to the maxProfit method, it can be set as a private nested class in Solution.

The full refactor is as follows:

Transaction firstTransaction = new Solution.Transaction(prices[0]);
Transaction secondTransaction = new Solution.Transaction(prices[0]);

for (int i =0; i< prices.length; i++){
    int price = prices[i];

    firstTransaction.optimizeLoss(price);
    firstTransaction.optimizeGain(price);

    secondTransaction.optimizeLoss(price - firstTransaction.gain);
    secondTransaction.optimizeGain(price);
}

return secondTransaction.gain;
package com.programming_grimoire.buy_sell_stock_iii;

class Solution {

  private class Transaction {
  private     int gain;
  private     int loss;

     public Transaction(int initialPrice){
         this.gain = 0;
         this.loss = initialPrice;
     }

     public void optimizeLoss(int price){
          if (price < loss ){
            this.loss = price;
          }
     }

     public void optimizeGain(int price){
         int net = price - this.loss;
         if (net > this.gain){
        this.gain = net;
         }
     }
   }

  public int maxProfit(int[] prices) {

    Transaction firstTransaction = new Solution.Transaction(prices[0]);
    Transaction secondTransaction = new Solution.Transaction(prices[0]);

    for (int i =0; i< prices.length; i++){
        int price = prices[i];

        firstTransaction.optimizeLoss(price);
        firstTransaction.optimizeGain(price);

        secondTransaction.optimizeLoss(price - firstTransaction.gain);
        secondTransaction.optimizeGain(price);
    }

    return secondTransaction.gain;
  }
}

Wax on. Wax off.

cd tdd_demo
mvn clean
mvn test
...
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s -- in com.programming_grimoire.buy_sell_stock_iii.SolutionTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
...

;;Functional code, do not change ;;Return to live text

Conclusion

Test-Driven Development is not a silver bullet6, however the constraints can be useful.

  1. They force the programmer to confirm the system is failing as expected. If you're expecting a test assertion failure and receive a compilation error, congratulations. You've just caught a bug early.
  2. They force the programmer to work on one darn thing at a time. That one darn thing can be anything from determining failure modes to defining a definite interface from a vague set of requirements.
  3. They force the programmer to re-evaluate and rewrite what they check in. Writing code is writing, and writing is re-writing.

Hopefully you'll find some use as well.

1

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii

2

Beck, Kent. Test-Driven Development By Example Addison-Wesley, September 2007

3

https://support.leetcode.com/hc/en-us/articles/360011833974-What-are-the-environments-for-the-programming-languages

4

https://maven.apache.org/

5

https://sources.debian.org/data/main/j/junit4/4.10-3/doc/cookstour/cookstour.htm

6

Brooks, Frederick P. (1986).