---
title: "Jam 07 - Exercise 1: Interface Implementation & Testing"
tags:
- 2 π in writing
- 3 π§ͺ in testing
- 4 π₯³ done
- inheritance
- interfaces
- polymorphism
- uml
- abstract-classes
- junit
---
<!-- markdownlint-disable line-length single-h1 no-inline-html -->
<!-- markdownlint-configure-file { "ul-indent": { "indent": 4 }, "link-fragments": {"ignore_case": true} } -->
{%hackmd dJZ5TulxSDKme-3fSY4Lbw %}
# Getting Started
Before diving into the exercises, let's set up our workspace:
1. Make sure you have committed all previous work
2. Create and switch to a new feature branch called `jam07`
3. Set up your source directory at `src/main/java/jam07`
4. Set up your test directory at `src/test/java/jam07`
:::info
π§ **Environment Setup Tips**
- Verify your working directory is clean before creating the branch
- Check your current branch in IntelliJ's bottom-right corner
- Remember to create both main and test directories
- Review Jam 01's documentation if you need git command help
:::
Now that we have our workspace ready, let's begin exploring inheritance, interfaces, and JUnit testing!
# A Note About UML Diagrams
In previous jams, you created and maintained UML class diagrams showing relationships between your classes. For this jam, you are **not required** to maintain or update those class diagrams. This allows you to focus on learning new concepts like inheritance, interfaces, and JUnit testing.
:::info
π¨ **Friday Challenge**
If you'd like to challenge yourself and earn extra credit, you can:
1. Maintain your UML class diagram from previous jams
2. Add the new classes and relationships from Jam07
3. Submit the updated diagram as `Jam07-class-uml.drawio.png`
This extra credit opportunity can enhance your grade for this jam, but not completing it won't negatively impact your base grade. The extra credit points will be added on top of your earned points for the required work.
:::
However, this jam will introduce you to a new type of UML diagram: **State Diagrams**. These diagrams help visualize how objects change state over time and what triggers those changes. You **will** be required to create a state diagram in Exercise 2 to model the behavior of the Register.
:::success
π **UML Focus for Jam07**
Required:
- Create UML state diagram for Register behavior (Exercise 2)
- Document state transitions and guards
- Show composite states
Optional (Extra Credit):
- Maintain and update class diagram
- Add new classes and relationships
- Document inheritance hierarchy
:::
# Exercise 1 - Interface Implementation & Testing
## Overview - Exercise 1
In this exercise, you'll create your first interface and implement it in multiple classes. Building on your experience with provided tests in Jams 05 and 06, you'll see how interfaces allow us to define common behavior across different types of objects.
:::info
π **Key Concepts**
- Interface design and implementation
- Polymorphic behavior
- Search functionality design
- Test-driven development
- Code documentation
:::
## The Problem - Exercise 1
Our retail system needs a flexible way to search for different types of objects (products, transactions, employees). While each object type has different attributes, they all need to be searchable. This is a perfect use case for an interface!
**Requirements**:
1. Create a `Searchable` interface that defines a contract for search functionality
2. Implement the interface in both Product and Transaction classes
3. Add search functionality to ProductCatalog using the interface
4. Add generic search functionality to Register for searching transactions
5. Write test cases to validate both implementations
6. Document the interface and implementation decisions
This scenario demonstrates why interfaces are valuable - they allow us to define common behavior across different types of objects, even when those objects need to implement the behavior in different ways.
## Understanding Interfaces
An interface defines a contract that classes can implement. Think of it like a promise - any class that implements an interface promises to provide all the behavior defined by that interface.
For example, our `Searchable` interface will define what it means to be "searchable":
- Products will be searchable by name or SKU
- Transactions will be searchable by timestamp, amount, or state
- (Later, Registers will be searchable by name or ID)
Even though each class implements search differently, they all share the common contract defined by the interface.
## Understanding Polymorphism and Generics
Now that we understand what we want to build, let's think about how to implement it. We want to create a reusable way to search any list of searchable items. But how do we write one method that could work with both Products AND Transactions? π€
Your first instinct might be to use regular polymorphism:
```java
// First attempt - using polymorphism
private List<Searchable> searchList(List<Searchable> items, String query) {
List<Searchable> results = new ArrayList<>();
for (Searchable item : items) {
if (item.matches(query)) {
results.add(item);
}
}
return results;
}
```
This seems reasonable! After all, both Product and Transaction implement Searchable, so this should work... right?
The problem appears when we try to use the results:
```java
List<Searchable> results = searchList(completedTransactions, "query");
Transaction firstMatch = results.get(0); // Error! Can't convert Searchable to Transaction
```
We've lost the specific type information! Even though we know these are actually Transaction objects, Java doesn't - it only sees them as generic Searchable objects. We could try to convert them back to Transactions using casting, but that's error-prone - what if we accidentally tried to cast a Product to a Transaction? Our code would crash!
So... how do we make something that could safely return `Product`s OR `Transaction`s without risking these kinds of errors? π€
Let's look at how we've used `ArrayList` before. We've used it like:
```java
ArrayList<String> names = new ArrayList<>(); // holds Strings
ArrayList<Product> products = new ArrayList<>(); // holds Products
```
We've been using those angle brackets `<...>` to tell ArrayList what kind of thing it should hold.
Have you ever wondered how ArrayList can work with any type we give it? What if we could do the same thing with our search method? We saw that our first thought might be "this method returns Searchable", but that led to casting problems. Instead, what if we could make the return type itself be a variable - something we could set to Product or Transaction or whatever type we need?
Enter Generics! π
When you look at the Java API documentation for ArrayList, you'll see it's defined as `ArrayList<T>`. That `<T>` is called a "type parameter" - it's a placeholder that gets replaced with whatever type we want to use. When we write `ArrayList<String>`, we're telling Java "replace T with String everywhere in ArrayList".
We can use this same idea for our method. Instead of using raw Searchable objects, we can tell Java "this method works with any type that is Searchable, but keep track of exactly which type it is":

Let's break this down:
- (1) Parameter: `List<T> items` means "this method takes a list and whatever type that list is, T is set to that Type"
- (2) Type Parameter Declaration: `<T extends Searchable>` means "T is restricted to any type that can be referred to as Searchable"
- (3) Return Type: `List<T>` means "this method returns a list of whatever T is"
This would result in:
```java
// Second attempt - using generics
default <T extends Searchable> List<T> searchList(List<T> items, String query) {
List<T> results = new ArrayList<>();
for (T item : items) {
if (item.matches(query)) {
results.add(item);
}
}
return results;
}
```
This way, when we search transactions, we get back a `List<Transaction>`, and when we search products, we get back a `List<Product>`. The compiler maintains type safety while still allowing us to write one reusable method!
## Step 1: Create the Searchable Interface
Design and implement a `Searchable` interface according to this UML (`searchList` implementation can be found above):
```plantuml
@startuml
+interface Searchable<T> {
+matches(query: String): boolean
+searchList(items: List<T>, query: String): List<T>
}
@enduml
```
Create this `public` interface in a new file called `Searchable.java` in your `jam07` package. Including the package statement, any imports, and curly brackets, you should have 4 lines of actual code (not counting any JavaDocs).
:::info
π **Default Methods in Interfaces**
Java 8 introduced default methods in interfaces, which allow us to provide a default implementation for interface methods. This is particularly useful for utility methods that would be implemented the same way by all implementing classes. In our case, the `searchList` method is a perfect candidate for a default method since it works the same way regardless of the implementing class.
:::
## Step 2: Implement in Product Class
Modify your existing `jam05.Product` class to implement the `jam07.Searchable` interface. You'll need to add the interface to the class declaration and implement the matches method. The matches method should check both the sku and the name of the product.
Consider these implementation decisions (feel free to start a discussion on Discord (or contribute to the existing one if someone already started it)):
- What should happen with null or empty queries?
- Should matches be case-sensitive?
- Should partial matches be allowed?
## Step 3: Implement in Transaction Class
Modify your existing `jam06.Transaction` class to implement the `jam07.Searchable` interface. You'll need to add the interface to the class declaration and implement the matches method. The matches method should check:
- Current state *(what method could you use to convert an enum to a string?)*
- Timestamp *(what method would convert this to a searchable string?)*
- Total amount *(what method would convert the total into a String currency representation? Did we already create a utility for this?)*
Consider the same implementation decisions as with `Product`:
- Null/empty query handling
- Case sensitivity
- Partial matches
## Step 4: Implement in Register Class
Modify your existing `jam06.Register` class to implement the `jam07.Searchable` interface. You'll need to add the interface to the class declaration and implement the matches method. The matches method should check:
- Register name *(what method would convert this to a lowercase searchable string?)*
- Drawer balance *(what method would convert this number into a searchable string?)*
Consider the same implementation decisions as with `Product`:
- Null/empty query handling
- Case sensitivity
- Partial matches
## Step 5: Add Searchable to ProductCatalog
You might be wondering why we need to make `ProductCatalog` implement the `Searchable` interface when we don't actually want to search for catalogs. The reason is that we want to use the interface's default `searchList` method to search through our products.
Think of it like borrowing a tool: to use the `searchList` tool that we created in the `Searchable` interface, our class needs to "sign the contract" (implement the interface) to get access to it. Even though we won't use all parts of the contract (the `matches` method), the benefit of getting access to `searchList` makes it worthwhile.
Modify your existing `jam05.ProductCatalog` class to implement the `jam07.Searchable` interface. Then add a search method that uses the interface's default `searchList` method. This method should search through the catalog's products using the `matches` method from the `Searchable` interface.
```java
public class ProductCatalog implements Searchable<Product> {
// ... existing code ...
@Override
public boolean matches(String query) {
// ProductCatalog itself isn't searchable, but we need to implement this
// method since we're implementing the interface
throw new UnsupportedOperationException("ProductCatalog does not support direct matching");
}
public List<Product> searchProducts(String query) {
// Use the default searchList method from the Searchable interface
return searchList(products, query);
}
}
```
:::info
π **Implementation Notes**
- The class must implement `Searchable<Product>` to use the default `searchList` method
- We implement `matches()` but throw an exception since ProductCatalog itself isn't searchable
- The `searchProducts` method uses the inherited default `searchList` method
- The search will work with both exact and partial matches based on Product's implementation
- Case sensitivity matches your implementation in Product
- This is a simpler, more straightforward implementation than writing our own search logic
:::
## Step 6: Add Transaction Search Method
Add a method to search transactions in the Register class according to this UML:
```plantuml
@startuml
class Register {
+searchTransactions(query: String): List<Transaction>
}
@enduml
```
:::info
π **Implementation Notes**
- Use the default searchList method from the Searchable interface (which we get for free as we've already implemented `Searchable` so we can search registers!)
- This method should search both completed and current transactions
- Remember that searchList takes a single list as input
- You'll need to figure out how to combine both transaction sources into one list (But don't modify your list of completed transactions!!!)
- Consider what happens if currentTransaction is null
- The method should return a List<Transaction> for type safety
- You will test this method in the next step
:::
## Understanding JUnit
JUnit is a testing framework that helps us verify our code works correctly. You've seen JUnit tests in previous jams, but now it's time to write your own! Let's break down the key components:
### Test Class Structure
Unlike regular Java classes, test classes are exempt from the Javadoc requirement. However, this exemption comes with a responsibility: test code must be self-documenting. This means:
- Test method names must clearly describe what they are testing
- Test data setup should be obvious from the variable names
- Comments should only explain complex test scenarios or edge cases
- The Arrange-Act-Assert pattern should be clearly visible in the code structure
The Arrange-Act-Assert pattern is a standard way to structure test methods:
1. **Arrange**: Set up your test data and conditions
2. **Act**: Call the method you're testing
3. **Assert**: Verify the results
This pattern makes tests easier to read and maintain because it clearly separates what you're testing from how you're testing it. We'll see this pattern in action in the example below.
Here is an example of the structure of a test file without JavaDocs.
```java
package jam0X;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MyClassTest { //Usually named for the class you are testing
// Fields for test data
private MyClass myObject;
private List<String> testData;
// Runs before each test
@BeforeEach
void setUp() {
myObject = new MyClass();
testData = new ArrayList<>();
}
// Runs after each test
@AfterEach
void tearDown() {
// Clean up test data - Not always necessary
myObject = null;
testData.clear();
}
// Individual test methods
@Test
void testSomething() {
// Arrange: Set up test data
// Act: Call the method being tested
// Assert: Verify the result is what you expect
}
}
```
### Key Concepts
1. **Test Methods**
- Must be annotated with `@Test`
- Follow the Arrange-Act-Assert pattern
- Use descriptive names that explain what's being tested
- Should test one specific behavior or a tightly coupled set of related behaviors
- Note: the tests you've seen so far lean more towards the tighly coupled approach. However, you may find it more useful to write for the specific behaviour approach for the advantages listed below
:::info
π‘ **Testing One vs. Many Behaviors**
There are two approaches to structuring test methods:
1. **Single Behavior Tests**
```java
@Test
void shouldStartEmpty() {
assertTrue(catalog.isEmpty());
assertEquals(0, catalog.getProductCount());
}
@Test
void testWithOneProduct() {
catalog.addProduct(testProduct1);
assertEquals(1, catalog.getProductCount());
}
@Test
void testWithTwoProducts() {
catalog.addProduct(testProduct1);
catalog.addProduct(testProduct2);
assertEquals(2, catalog.getProductCount());
}
```
- Tests one specific behavior
- Makes failures easy to diagnose
- Best for simple behaviors
2. **Related Behaviors Tests**
```java
@Test
void testCountAndEmpty() {
// Test empty catalog
assertTrue(catalog.isEmpty());
assertEquals(0, catalog.getProductCount());
// Test with one product
catalog.addProduct(testProduct1);
assertFalse(catalog.isEmpty());
assertEquals(1, catalog.getProductCount());
// Test with multiple products
catalog.addProduct(testProduct2);
assertFalse(catalog.isEmpty());
assertEquals(2, catalog.getProductCount());
}
```
- Tests multiple related behaviors
- Useful when behaviors are tightly coupled
- Can be more efficient for testing state transitions
- Best when the behaviors are part of the same concept
Choose based on:
- How related the behaviors are
- How complex each behavior is
- How important it is to isolate failures
- Whether the behaviors are part of the same concept
:::
2. **Setup & Teardown**
- `@BeforeEach` runs before each test
- Use it to create fresh test objects
- Avoid sharing state between tests
- `@AfterEach` runs after each test
- Use it to clean up resources
- Close files, connections, etc.
- This is not always necessary
3. **Assertions**
```java
// Common assertions, there are many others!
assertEquals(expected, actual); // Values should be equal
assertTrue(condition); // Condition should be true
assertFalse(condition); // Condition should be false
assertNull(object); // Object should be null
assertNotNull(object); // Object should not be null
assertThrows(Exception.class, () -> { // Code should throw exception
// code that should throw
});
```
4. **Test Organization**
- Group related tests together
- Use descriptive method names
- Follow a consistent pattern
- Test both normal and edge cases
### Example: Testing the Searchable Interface
Let's look at how we might test the `matches` method in Product:
```java
@Test
void shouldMatchExactProductName() {
// Arrange
Product product = new Product("Test Product", "TEST123", 9.99);
// Act
boolean result = product.matches("Test Product");
// Assert
assertTrue(result);
}
@Test
void shouldNotMatchDifferentName() {
// Arrange
Product product = new Product("Test Product", "TEST123", 9.99);
// Act
boolean result = product.matches("Different Name");
// Assert
assertFalse(result);
}
```
### Testing Tips
1. **Choose Your Testing Approach**
- Single Behavior Tests: Best for simple behaviors and easy failure diagnosis
- Related Behaviors Tests: Best for tightly coupled behaviors and state transitions
- Consider the complexity and relationship of behaviors when choosing
- When in doubt, choose single behavior tests - they're easier to maintain and debug, both for the tests themselves and the code they're testing
2. **Use Descriptive Names**
- Test names should explain what's being tested
- Example: `shouldMatchExactProductName` instead of `testName`
- For single behaviors, use names that focus on the specific outcome
- For related behaviors, consider using names that reflect the relationship
3. **Follow Arrange-Act-Assert**
- Arrange: Set up your test data
- Act: Call the method being tested
- Assert: Verify the results
- For single behaviors, keep setup minimal and focused
- For related behaviors, consider using comments to separate different phases
4. **Test Edge Cases**
- Null values
- Empty strings
- Boundary conditions
- Invalid inputs
- For single behaviors, create separate tests for each edge case
- For related behaviors, consider whether edge cases should be separate tests or part of a related behaviors test
5. **Keep Tests Independent**
- Each test should work on its own
- Don't rely on state from other tests
- Use `@BeforeEach` for common setup
- For single behaviors, ensure setup is complete and self-contained
- For related behaviors, ensure each phase builds on the previous one clearly
## Step 7: Write Test Cases
Now that you understand how to write effective tests, it's time to put that knowledge into practice! You'll be creating two test files to validate your implementations of the `Searchable` interface:
1. `ProductTest.java`
2. `TransactionTest.java`
3. `ProductCatalogTest.java`
4. `RegisterTest.java`
Use the skeleton code below to get you started on these two files, then implement all the necessary test methods to thoroughly validate your Searchable interface implementations.
:::info
π¦ **Package Location Note**
In industry, test classes are typically placed in the same package as the classes they're testing. For example, if you're testing `jam05.Product`, the test would be in `jam05.ProductTest`. However, for grading purposes in this course, you must place these tests in the `jam07` package. This is an exception to standard practice, but it helps us grade your work correctly.
:::
Create test files for both implementations in the `jam07` folder:
### `ProductTest.java`
This is mostly set up to test seperate behaviours, however the last method combines some closely related tests. A few methods have been implemented for you. To save space, TODO comments were not left in every empty method.
```java
package jam07;
import static org.junit.jupiter.api.Assertions.*;
import jam05.Product;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ProductTest {
private Product product;
@BeforeEach
void setUp() {
// TODO: Initialize test product(s)
}
@Test
void shouldMatchExactProductName() {
// Arrange: Set up a query with a known name
String query = "Test Product";
// Act: Call matches() with the query
boolean result = product.matches(query);
// Assert: Verify the result is what you expect
assertTrue(result);
}
@Test
void shouldMatchPartialName() {
// Arrange: Set up a query with a known name
// Act: Call matches() with the query
// Assert: Verify the result is what you expect
}
@Test
void shouldMatchExactSku() {}
@Test
void shouldMatchPartialSku() {}
@Test
void shouldNotMatchDifferentName() {}
@Test
void shouldHandleNullQuery() {}
@Test
void shouldHandleEmptyQuery() {}
@Test
void shouldHandleWhitespaceInQuery() {
// Arrange: Set up a query with extra whitespace
String query = " Test Product ";
// Act: Call matches() with the query
boolean result = product.matches(query);
// Assert: Verify the result is what you expect
assertTrue(result);
}
@Test
void shouldMatchCaseInsensitive() {
// Arrange: Set up queries with different cases
String query1 = "test product";
String query2 = "TEST PRODUCT";
String query3 = "TeSt PrOdUcT";
// Act: Call matches() with each query
boolean result1 = product.matches(query1);
boolean result2 = product.matches(query2);
boolean result3 = product.matches(query3);
// Assert: Verify all results are true
assertTrue(result1);
assertTrue(result2);
assertTrue(result3);
}
}
```
### `TransactionTest.java`
```java
package jam07;
import static org.junit.jupiter.api.Assertions.*;
import jam05.Product;
import jam06.Transaction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class TransactionTest {
private Transaction transaction;
private Product testProduct;
@BeforeEach
void setUp() {
// TODO: Initialize transaction(s) and test product(s)
}
// TODO: Write tests for matches() method
// Consider testing:
// - State matching
// - Amount matching
// - Timestamp matching
// - Case sensitivity matching
// - Null/empty handling matching
// I ended up with 9 tests. You may have more or less!
}
```
### DETOURβ
Next we are going to test the ProductCatalog and Register methods we wrote.
While we could write ProductCatalogTest to look like:
```plantuml
@startuml
hide empty members
class ProductTest {
+shouldMatchExactProductName()
+shouldMatchPartialName()
+shouldMatchExactSku()
+shouldMatchPartialSku()
+shouldNotMatchDifferentName()
+shouldHandleNullQuery()
+shouldHandleEmptyQuery()
+shouldHandleWhitespaceInQuery()
+shouldMatchCaseInsensitive()
}
class ProductCatalogTest {
+shouldMatchExactProductName()
+shouldMatchPartialName()
+shouldMatchExactSku()
+shouldMatchPartialSku()
+shouldNotMatchDifferentName()
+shouldHandleNullQuery()
+shouldHandleEmptyQuery()
+shouldHandleWhitespaceInQuery()
+shouldMatchCaseInsensitive()
}
ProductTest -[hidden]down- ProductCatalogTest
note right of ProductCatalogTest
Duplicates all tests from ProductTest
Redundant testing
Harder to maintain
end note
@enduml
```
Or we could use the testing hierarchy approach:
```plantuml
@startuml
hide empty members
class ProductTest {
+shouldMatchExactProductName()
+shouldMatchPartialName()
+shouldMatchExactSku()
+shouldMatchPartialSku()
+shouldNotMatchDifferentName()
+shouldHandleNullQuery()
+shouldHandleEmptyQuery()
+shouldHandleWhitespaceInQuery()
+shouldMatchCaseInsensitive()
}
note right of ProductTest
Tests detailed matching logic
Covers all edge cases
Focuses on single object behavior
end note
ProductTest -[hidden]down- ProductCatalogTest
class ProductCatalogTest {
+shouldFindMultipleMatchingProducts()
+shouldReturnEmptyListForNoMatches()
+shouldReturnEmptyListForNullQuery()
}
note right of ProductCatalogTest
Tests collection behavior
Verifies use of Product.matches()
Focuses on multiple objects
Allows for the edge case tests of the Product.matches() to stay in the ProductTest file
end note
@enduml
```
:::info
π‘ **Testing Hierarchy**
The test classes follow a clear hierarchy:
1. **ProductTest & TransactionTest**
- Test the detailed implementation of `matches()`
- Cover all edge cases and matching scenarios
- Focus on the specific matching logic
2. **ProductCatalogTest & RegisterTest**
- Test only the collection-level behavior
- Verify correct use of the underlying `matches()` method
- Focus on handling multiple items and edge cases
This approach:
- Avoids redundant testing
- Makes test failures easier to diagnose
- Follows the principle of testing at the most specific level
- Keeps each test class focused on its unique responsibilities
:::
### `ProductCatalogTest.java`
Note: All but one method is implemented for you. You'll need to implement the next to last one.
```java
package jam07;
import static org.junit.jupiter.api.Assertions.*;
import jam05.Product;
import jam05.ProductCatalog;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ProductCatalogTest {
private ProductCatalog catalog;
private Product product1;
private Product product2;
private Product product3;
@BeforeEach
void setUp() {
catalog = new ProductCatalog();
product1 = new Product("Red Apple", "FRUIT001", 0.99);
product2 = new Product("Green Apple", "FRUIT002", 0.89);
product3 = new Product("Banana", "FRUIT003", 0.79);
catalog.addProduct(product1);
catalog.addProduct(product2);
catalog.addProduct(product3);
}
@Test
void shouldFindMultipleMatchingProducts() {
// Arrange: Set up a query that matches multiple products
String query = "Apple";
// Act: Call searchProducts with the query
List<Product> results = catalog.searchProducts(query);
// Assert: Verify the result contains both apples but not the banana
assertEquals(2, results.size());
assertTrue(results.contains(product1));
assertTrue(results.contains(product2));
assertFalse(results.contains(product3));
}
@Test
void shouldReturnEmptyListForNoMatches() {
// TODO: Implement this test
// Arrange: Set up a query that matches no products
// Act: Call searchProducts with the query
// Assert: Verify the result is an empty list
}
@Test
void shouldReturnEmptyListForNullQuery() {
// Arrange: Use null as the query
String query = null;
// Act: Call searchProducts with null
List<Product> results = catalog.searchProducts(query);
// Assert: Verify an empty list is returned
assertTrue(results.isEmpty());
}
}
```
### `RegisterTest.java`
π§ **Helper Methods in Test Setup**
This test class introduces helper methods to make the setup code more organized and reusable:
1. `initializeCashDrawer()`: Sets up the register's cash drawer with test money
2. `completeTransaction()`: Creates and completes a transaction with a test product
These helper methods:
- Reduce code duplication
- Make the setup code more readable
- Make it easier to modify test setup in one place
- Follow the DRY (Don't Repeat Yourself) principle
You can use this same pattern in your own test classes to make them more maintainable!
π’ **Static Register Counter**
The test class also introduces a static counter (`registerCounter`) to ensure each test gets a unique register name. This is important because:
- The Register class uses a static factory method (`createRegister`)
- Each register must have a unique name
- Multiple tests running in parallel could create name conflicts
- The counter ensures names like "TEST-001", "TEST-002", etc.
This is a common pattern when testing classes that use static factory methods or require unique identifiers.
:::warning
β οΈ **Method Call Changes**
Just like in Jam06's RegisterDriver, you may need to make changes to method calls to resolve all the errors. The your Register class's API is probably different from mine, so you'll need to update any method calls accordingly. However, you shouldn't change the test values.
:::
```java
package jam07;
import static org.junit.jupiter.api.Assertions.*;
import jam05.Product;
import jam05.ProductCatalog;
import jam06.Money;
import jam06.Register;
import jam06.Transaction;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class RegisterTest {
private static int registerCounter = 0;
private Register register;
private Product testProduct;
private Transaction transaction1;
private Transaction transaction2;
@BeforeEach
void setUp() {
// Set up catalog and register
ProductCatalog catalog = new ProductCatalog();
testProduct = new Product("Test Product", "TEST123", 9.43);
catalog.addProduct(testProduct);
// Create register with unique name
String registerName = String.format("TEST-%03d", ++registerCounter);
register = Register.createRegister(registerName, catalog);
// Initialize cash drawer
initializeCashDrawer();
// Complete two transactions
transaction1 = completeTransaction();
transaction2 = completeTransaction();
}
private void initializeCashDrawer() {
// Add money denominations individually to avoid null issues
register.addMoneyToDrawer(Money.TWENTY, 5); // $100
register.addMoneyToDrawer(Money.TEN, 10); // $100
register.addMoneyToDrawer(Money.FIVE, 20); // $100
register.addMoneyToDrawer(Money.ONE, 50); // $50
}
private Transaction completeTransaction() {
register.startTransaction();
register.addProductToTransaction(testProduct.getSku());
Transaction thisTransaction = register.getCurrentTransaction();
register.processPayment(Map.of(Money.TEN, 1)); // $10 payment for $9.00 item
return thisTransaction;
}
@Test
void shouldReturnEmptyListForNoMatches() {
// TODO: Implement this test
// Arrange: Set up a query that matches no transactions
// (Hint: Transaction.matches() checks state, amount, and timestamp)
// Act: Call searchTransactions with the query
// Assert: Verify the result is an empty list
}
@Test
void shouldFindMultipleMatchingTransactions() {
// Arrange: Set up a query that matches both transactions
String query = "$10.00"; // Search by amount
// Act: Search for transactions
List<Transaction> results = register.searchTransactions(query);
// Assert: Both transactions should be found
assertEquals(2, results.size());
assertTrue(results.contains(transaction1));
assertTrue(results.contains(transaction2));
}
@Test
void shouldIncludeCurrentTransactionInSearch() {
// Arrange: Start a new transaction that matches search
register.startTransaction();
register.addProductToTransaction(testProduct.getSku());
Transaction currentTransaction = register.getCurrentTransaction();
String query = "IN_PROGRESS"; // Search by state
// Act: Search for transactions
List<Transaction> results = register.searchTransactions(query);
// Assert: Current transaction should be included
assertTrue(results.contains(currentTransaction));
}
@Test
void shouldReturnEmptyListForNullQuery() {
// Arrange: Use null as the query
String query = null;
// Act: Search with null query
List<Transaction> results = register.searchTransactions(query);
// Assert: Should return empty list
assertTrue(results.isEmpty());
}
}
```
:::info
π‘ **Testing Tips**
- Follow the Arrange-Act-Assert pattern in your tests
- Test one specific behavior per test method
- Use clear, descriptive test method names
- Consider edge cases (null values, empty strings, etc.)
- Test both positive and negative cases
- Use appropriate assertions for the type of test
:::
## Exercise 1 Checkpoint
> π **Checkpoint**
>
> Before proceeding, verify:
>
> **Interface Implementation**
>
> - `Searchable` interface is properly defined
> - Both `Product` and `Transaction` implement the interface
> - `ProductCatalog` uses the interface for searching
> - Generic `searchList` method is implemented in `Register`
> - `searchTransactions` method is implemented in `Register`
>
> **Testing Implementation**
>
> - All test cases are implemented and pass
> - Tests follow the hierarchy (unit tests vs collection tests)
> - Tests use helper methods and static counters appropriately
> - Tests follow Arrange-Act-Assert pattern
> - Tests cover both normal and edge cases
>
> **Code Quality**
>
> - Documentation is complete
> - Code follows Java naming conventions
> - All files are in the correct packages
> - All changes are committed
## Save Your Work - Exercise 1
**Run all tests to verify your implementation**:
```bash
# Right click on the test folder in IntelliJ
# Click "Run Tests in 'csci205_jams'"
# Ensure all tests pass
```
**Stage your changes**:
```bash
# Jam05 files
git add src/main/java/jam05/Product.java src/main/java/jam05/ProductCatalog.java
# Jam06 files
git add src/main/java/jam06/Register.java src/main/java/jam06/Transaction.java
# Jam07 files
git add src/main/java/jam07/Searchable.java src/test/java/jam07/ProductCatalogTest.java src/test/java/jam07/ProductTest.java src/test/java/jam07/RegisterTest.java src/test/java/jam07/TransactionTest.java
```
**Verify what files are uncommitted**:
```bash
git status
```
**Commit your work**:
```bash
git commit -m "jam07: Implement Searchable interface in Product and Transaction"
```
:::success
π **Key Takeaways**
- Interfaces define behavior contracts
- Different classes can implement the same interface differently
- Interfaces enable polymorphic behavior
- Good tests verify both common and unique behaviors
- Documentation helps others understand your design decisions
:::