-
-
Save flaviusstef/725342 to your computer and use it in GitHub Desktop.
package ca.jbrains.pos.test; | |
import org.junit.*; | |
import static org.junit.Assert.*; | |
import java.util.*; | |
public class SellOneItemTest { | |
private POSDisplay display = new POSDisplay(); | |
private Sale sale; | |
public static class Sale { | |
private POSDisplay display; | |
private Map<String, String> pricesByBarcode = new HashMap<String, String> (); | |
public Sale(POSDisplay d, Map<String, String> pricesByBarcode) { | |
if (pricesByBarcode == null) { | |
throw new RuntimeException("Invalid price list"); | |
} | |
display = d; | |
this.pricesByBarcode = pricesByBarcode; | |
} | |
public void onBarcode(String barcode) { | |
if ("".equals(barcode)) { | |
display.onEmptyBarcode(); | |
return; | |
} | |
if (!pricesByBarcode.containsKey(barcode)) { | |
display.onInvalidBarcode(barcode); | |
return; | |
} | |
display.showPrice(pricesByBarcode.get(barcode)); | |
} | |
} | |
public static class Display { | |
private String text; | |
public void setText(String text) { | |
this.text = text; | |
} | |
public String getText() { | |
return text; | |
} | |
} | |
public static class POSDisplay extends Display { | |
public void onEmptyBarcode() { | |
setText("Scanning error: empty barcode"); | |
} | |
public void onInvalidBarcode(String barcode) { | |
setText("No product with barcode " + barcode); | |
} | |
public void showPrice(String price) { | |
setText(price.equals("$0.00") ? "FREE" : price); | |
} | |
} | |
@SuppressWarnings("serial") | |
@Before | |
public void setUp() { | |
sale = new Sale(display, new HashMap<String, String>() { | |
{ | |
put("123", "$12.50"); | |
put("456", "$20.00"); | |
put("333", "$0.00"); | |
} | |
}); | |
} | |
@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 { | |
assertForBarcodeDisplayShows("333", "FREE"); | |
} | |
@Test(expected=RuntimeException.class) | |
public void nullProductListIsInvalid() { | |
new Sale(display, null); | |
} | |
private void assertForBarcodeDisplayShows(String barcode, String message) { | |
sale.onBarcode(barcode); | |
assertEquals(message, display.getText()); | |
} | |
} |
Some questions.
- Why
setPricesByBarcode()
? I don't think this extra complication in the design adds value to any production client. - Why
_pricesByBarcode == null
? Why allow a required field ever to benull
? - I think I can delete
removeBarcode()
without failing any test, so why did you write it? - Why do I have to scan a barcode to check formatting $0 as "FREE"?
- Why prefix fields with underscore?
Thank you for the feedback.
Refactored:
- Forgotten it in, unnecessary, removed it.
- Extraneous check, condition is never true. Removed it.
- I was wrong. Added code without test. For symmetry.
- Correct. The test fixture was too big. Minimized it.
- It's a convention I defined for this problem. I researched the tubes, found out Sun discourages underscores apart from constant names. My idea is "_fieldname" is shorter than "this.fieldname". Perhaps you'd like to share the advantages of using "this." if that was the purpose of the question.
First, a response to your comment about this.field
or _field
. I typically simply use field
except when I assign a new value to field
, when I tend to write this.field = field
because I tend to name the method parameter field
as well.
Examples:
public Object(Collaborator collaborator) {
this.collaborator = collaborator;
}
public void doSomething() {
collaborator.doSomethingElse();
}
Next, thanks for playing along. I hope this helps you.
Now, some new questions.
- You had added
removeBarcode()
"for symmetry". What about that symmetry did/do you value? - Why
addProduct()
? I don't think this extra complication in the design adds value to any production client. - Why
removeProduct()
? I don't think this extra complication in the design adds value to any production client. - Why do I have to find a $0-priced product in the
Catalog
to check formatting $0 as"FREE"
? - Why did you write the test
emptyCatalogueIsSearchable()
? I don't see the value in it. - Why did you write the test
itemsCanBeAddedAndRemovedFromCatalogue()
? I don't see the value in it. - Why do you assign a value to the field
_display
twice - once in the field definition and once in the@Before
method?
Thank you for bearing with me.
0 Since I don't professionally develop in Java, I take the word of a much wiser man and give up the underscore for field names.
removeBarcode()
doesn't exist, I guess we are actually referring toremoveProduct()
- If you are implying that I did not need it as per the requirements of the problem/YAGNI rule, then you are right. But as I like interfaces to be symmetrical,Catalogue
was blessed with bothaddProduct()
andremoveProduct()
. Is symmetry in this case, in your oppinion, overrated ?- Who then, should encapsulate the list of products? Or do you consider the initial variant (
Sale
receiving aMap
in the constructor) as being simpler? - see 2.
- Are you implying the need for a
Product
class, whose responsibility it would be to map between "$0" and "FREE"? I view that mapping as being more of a marketing stunt, and as such the responsibility ofCatalogue
. - Added during development, as I was unsure of what
Map.containsKey()
does on an emptyMap
. Removed it. - That's the test code that covers
addProduct()
andremoveProduct()
. Have refactored it because it was testing the SUT through a DOC. I imagine your objection being linked to numbers 2. and 3. I haven't yet figured out the path you'd like to take me on, so a little bit more explicitness would be very welcomed. - My mistake. Removed the double.
Thank you for your willingness to think about my comments. I don't judge; I simply offer comments and ask people to reflect on them.
-
I did mean
removeProduct()
. You say you like interfaces to be symmetrical. I don't value symmetry on its own, except perhaps in an API I publish for third-party use. Especially for this first feature, I don't see the value of symmetry in this case. -
I don't see the need to encapsulate the collection of
Product
s yet. I do prefer simply letting theSale
takepricesByBarcode
in its constructor. If you have any concerns about clients changing the contents of theMap
without your knowledge, then consider storing either a copy or an unmodifiable view. (Java offers a way to mark a collection as unmodifiable.) -
I don't mean that we need a
Product
class yet. I merely suggest that I shouldn't have to look up a product of price $0 in order to format the price as "FREE". I find it strange that aCatalogue
would take responsibility for presenting a price as text. That sounds like a view-related responsibility. -
When I don't know what an API method does, like
Map.containsKey()
, I write a Learning Test for it, like this:public class MapContainsKeyTest { @Test public void emptyMap() { assertFalse(new HashMap().containsKey("anything")); } }
-
I understand your impulse to test
addProduct()
andremoveProduct()
directly, but if pass thepricesByBarcode
into the constructor forSale
, you can delete these methods and their corresponding tests.
Here's another go, starting from scratch.
- Moved all presentation logic (string generation) to a
Display
son,POSDisplay
. I am interested to see whether you consider this responsibility (special formatting of empty barcode, invalid barcode and free product) to be in the right place(POSDisplay
), or instead as beingSale
's responsibility. - Created
assertForBarcodeDisplayShows()
, stealing the idea from one of the other participants. - Enforced the necessity of a non-null
pricesByBarcode()
via an exception.
Side note: I have a nagging voice in my head telling me that, with the tests in their current situation, Display
gets exercised only via Sale
. Is the voice right? Should I be adding tests for POSDisplay
?
First, my own observations, and then my comments on your comments.
- Why the inconsistency between lines 20 and 21? If you plan to write
this.x = x;
in constructors, then I recommend doing it uniformly. - I'd like the exception you throw on a
null
pricesByBarcode
to describe the problem more precisely. - You have decided to design
Sale
to require non-null
apricesByBarcode
even though there is a code path throughonBarcode()
that doesn't requirepricesByBarcode
. If you sometimes need one and sometimes don't, what might that say about the design? - I see that you've moved methods responsible for displaying something into
POSDisplay
, which I quite like. - I wonder why you named the methods in
POSDisplay
inconsistently. You have the imperativeshowPrice()
but the event handlingonInvalidBarcode()
andonEmptyBarcode()
. - You named the method
onInvalidBarcode()
, but is a barcode not in our catalog truly invalid? What other method name would describe this special case more accurately? - Why have
Display
andPOSDisplay
as separate classes? - I see that formatting $0 as
"FREE"
now appears in the view, which I find quite sensible. Now look at the methodshowPrice(String price)
. Should we model a price as text?
Otherwise, I like what you've done, and especially assertForBarcodeDisplayShows()
.
Regarding whether to write tests directly for POSDisplay
, I think you can survive without it for a moment.
I decided to make a small number of improvements at https://gist.github.com/731349 to show you an example of how I approach the code you're provided here. You can clone the entire repository to step through the commits and examine the differences.
Steps taken:
Possible future abstractions: Product, Price.