-
-
Save jbrains/731349 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ackage ca.jbrains.pos.test; | |
import org.junit.*; | |
import static org.junit.Assert.*; | |
import java.util.*; | |
public class SellOneItemTest { | |
private POSDisplay posDisplay = new POSDisplay(); | |
private Sale sale; | |
public interface Catalog { | |
Price lookupPrice(String barcode); | |
boolean hasBarcode(String barcode); | |
} | |
public static class InMemoryCatalog implements Catalog { | |
private Map<String, Price> pricesByBarcode; | |
public InMemoryCatalog(Map<String, Price> pricesByBarcode) { | |
if (pricesByBarcode == null) { | |
throw new IllegalArgumentException("pricesByBarcode = " + pricesByBarcode); | |
} | |
this.pricesByBarcode = pricesByBarcode; | |
} | |
public Price lookupPrice(String barcode) { | |
return pricesByBarcode.get(barcode); | |
} | |
public boolean hasBarcode(String barcode) { | |
return pricesByBarcode.containsKey(barcode); | |
} | |
} | |
public static class Sale { | |
private POSDisplay posDisplay; | |
private Catalog catalog; | |
public Sale(POSDisplay posDisplay, Catalog catalog) { | |
this.posDisplay = posDisplay; | |
this.catalog = catalog; | |
} | |
public void onBarcode(String barcode) { | |
if ("".equals(barcode)) { | |
posDisplay.displayScannedEmptyBarcodeMessage(); | |
return; | |
} | |
if (!catalog.hasBarcode(barcode)) { | |
posDisplay.displayProductNotFoundMessage(barcode); | |
return; | |
} | |
posDisplay.displayPrice(catalog.lookupPrice(barcode)); | |
} | |
} | |
public static class Price { | |
private int cents; | |
public Price(int cents) { | |
this.cents = cents; | |
} | |
public static Price inCents(int cents) { | |
return new Price(cents); | |
} | |
public String format() { | |
return cents == 0 ? "FREE" : NumberFormat.getCurrencyInstance().format(cents / 100.0d); | |
} | |
} | |
public static class POSDisplay { | |
private String text; | |
private void setText(String text) { | |
this.text = text; | |
} | |
public String getText() { | |
return text; | |
} | |
public void displayScannedEmptyBarcodeMessage() { | |
setText("Scanning error: empty barcode"); | |
} | |
public void displayProductNotFoundMessage(String barcode) { | |
setText("No product with barcode " + barcode); | |
} | |
public void displayPrice(Price price) { | |
setText(price.format()); | |
} | |
} | |
@SuppressWarnings("serial") | |
@Before | |
public void setUp() { | |
sale = new Sale(posDisplay, new InMemoryCatalog(new HashMap<String, Price>() { | |
{ | |
put("123", Price.inCents(1250)); | |
put("456", Price.inCents(2000)); | |
put("333", Price.inCents(0)); | |
} | |
})); | |
} | |
@Test | |
public void productFound() throws Exception { | |
assertForBarcodeDisplayShows("123", "$12.50"); | |
} | |
@Test | |
public void anotherProductFound() throws Exception { | |
assertForBarcodeDisplayShows("456", "$20.00"); | |
} | |
@Test | |
public void productNotFound() throws Exception { | |
assertForBarcodeDisplayShows("999", "No product with barcode 999"); | |
} | |
@Test | |
public void emptyBarcode() throws Exception { | |
assertForBarcodeDisplayShows("", "Scanning error: empty barcode"); | |
} | |
@Test | |
public void freeProductHasDistinctFormat() throws Exception { | |
assertEquals("FREE", Price.inCents(0).format()); | |
} | |
@Test(expected=RuntimeException.class) | |
public void nullProductListIsInvalid() { | |
new InMemoryCatalog(null); | |
} | |
private void assertForBarcodeDisplayShows(String barcode, String message) { | |
sale.onBarcode(barcode); | |
assertEquals(message, posDisplay.getText()); | |
} | |
} |
@mgenov, here are some answers.
- I chose
Sale
before I knew the class would become a controller. It seemed like a reasonable name for an object that represented the interaction of selling a single item. Typically, once we add features like selling multiple items, printing receipts, and so on, we extract the domain behavior into a smaller class calledSale
. - So far we only have two messages, and while I notice the duplication in the method names
display*Message()
, I typically don't introduce aMessage
object until the thirddisplay*Message()
method, using the "Three strikes and you refactor" rule.
Regarding introducing the SCANNING_ERROR
constant, I generally don't like to introduce constants that say the same thing as the values they replace. Instead, I'd rather remove the duplication by introducing a ScanningErrorMessage
class.
Sounds reasonable. Thanks @jbrains
btw I like the idea for the new project. These testing ideas, advices, practice examples and conversations are awesome.
Created project: https://github.com/flaviusstef/POS-TDD-Exercise
Let's have some fun there. :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@flaviusstef, let's move this to a github.com project to add features. Please start a project for yourself with whichever code base you want, and we'll use the Wiki there for features and all that. Email me when you have done that.