-
Notifications
You must be signed in to change notification settings - Fork 545
Split up and test Package and PackageItem #435
Conversation
) | ||
end | ||
|
||
def test_accessors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test was incomplete and wasn't really testing everything clearly. Replaced it with many other tests testing each feature explicitly.
@@ -0,0 +1,72 @@ | |||
module ActiveShipping #:nodoc: | |||
class PackageItem |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing in here changed. Just moved.
end | ||
|
||
def test_initialize_weight_mass_object | ||
begin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the tests that asserts the broken behaviour as a skip, but then will fail if it is no longer broken.
A first step toward #427 in 2.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great test suite! A few notes, but mostly I want us to use tighter deltas (to increase trust in this library) and no bare asserts/refutes.
assert_equal @weight, @item.grams | ||
end | ||
|
||
def test_initialize_negative_quantity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think test names are better when they outline the expectation, not the thing they're testing (both is fine too). Some examples:
test_initialize_with_negative_quantity_should_default_to_one
test_initialize_forces_positive_quantity
It becomes a little verbose, but also very clear what's happening in here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really dislike the def test_
DSL here. It's part of what makes minitest so hard to work with.
I'll be coming back and naming all the tests with test "#method_name action conditions" do
as part of 2.0. So I'm not really worried about the naming here.
refute_equal @weight, @item.ounces | ||
end | ||
|
||
def test_initialize_weight_does_not_accept_strings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for noting: I don't think this is a good behaviour. Either accepting both or raising would be preferred, but either way I'll be surprised to get metric here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these tests test inconsistent behaviour. We are in agreement.
|
||
def test_grams_value | ||
assert_equal 100, @item.grams | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems inconsistent. We have test_initialize_with_all_attributes
and then this one. We're testing the same behaviour (state on initialization), but across two tests.
This is likely a difference between classical unit testing and behaviour-driven testing, where the former focuses more on individual methods, but I'm more for the latter. What are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again. All part of a bad interface. In that the grams
method is both an accessor and performing logic. There is no way to test these pieces in isolation. So I have opted to be a bit redundant, and test what is available, and all edges of the public interface. However awkward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is part of a bad interface, but simply a mixture of testing paradigms. There's a paradigm where you focus on individual methods and the inputs/outputs of those. Then there's behavioural testing where you have expected behaviours of your classes and test those, which often means testing across methods / accessors / readers / etc. I'm a big fan of more of the latter, but it's totally fine to mix and match.
What's confusing to me, and the reason for this comment, is that we have test_initialize_with_all_attributes
test, which tests the state of things after initialization, and that's exactly what this test is also doing. We've taken a behaviour (state immediately after initialization) and split it across the wrong boundary (attr_reader
/ def foo
). Whether there's logic or not happening in those should be irrelevant to clients of the object. Also, there's nothing stopping us from "upgrading" one of the attr_reader
s to a manual def attr; ...stuff...; end
in the future.
Sorry for the wordiness, but hopefully that cleared up my stance :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P.S. I don't want to block on shipping because of this, but I do believe this would make for a higher quality and easier to understand test suite.
def test_ounces_converts | ||
@item = PackageItem.new(@name, @weight, @value, @quantity, @options.merge(units: :metric)) | ||
|
||
assert_in_delta 3.5, @item.ounces, 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, large delta.
def test_pounds_converts | ||
@item = PackageItem.new(@name, 1000, @value, @quantity, @options.merge(units: :metric)) | ||
|
||
assert_in_delta 2.2, @item.pounds, 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll just let you find all of these :)
flunk "This code path is broken but passed unexpectedly" | ||
rescue NoMethodError | ||
skip "This code path is broken" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of things:
- Although this is slightly different, I wonder would
assert_raises(NameError) { ... }
basically do the same thing for us? I guess it's not good at conveying what we want to convey here, since it says "we expect this to happen". - If we want this exactly, it's used enough that maybe a helper function in
TestHelper
would be nice?broken_with(NameError) { ... }
, or whatever name you please. I'm fine keeping these here too, since they'll hopefully disappear soon!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided against 1. But 2, that's what this does. It doesn't do the indirection through a helper, but it definitely is explicit in that we expect a failure, then skip the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm game for keeping this. Like I mentioned, this will hopefully disappear quickly :)
def test_grams_converts | ||
@item = PackageItem.new(@name, 100, @value, @quantity, @options.merge(units: :imperial)) | ||
|
||
assert_in_delta 2834.9, @item.grams, 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1 is a very large window of error, which makes me lose trust in the library. I'd tighten up the error bounds, e.g., assert_in_delta 2834.95, @item.grams, 0.01
.
end | ||
|
||
def test_unpackaged | ||
assert Package.new(@weight, @dimensions, unpackaged: true).unpackaged? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer avoiding a bare assert
/ refute
(bare meaning without an error message) since the failed assertion message is useless. assert_predicate Package.new(...), :unpackaged?
for this, and anywhere else here.
assert_equal @dimensions[0], @imperial_package.inches(:height) | ||
assert_equal @dimensions[0], @imperial_package.inches(:depth) | ||
assert_equal @dimensions[0], @imperial_package.inches(:high) | ||
assert_equal @dimensions[0], @imperial_package.inches(:deep) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for noting: I'm personally not a fan of this behaviour. It's adding unnecessary complexity / surface area to this library. Are they useful? My own opinion is that they aren't, and that we should let clients decide if they want to alias things.
assert_equal 12, Package.cents_from(12) | ||
end | ||
|
||
def test_cents_from_nonsense |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So much 😂 for this name. I think I'd be on the floor if it were noncents
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_inches_x | ||
assert_equal @dimensions[2], @imperial_package.inches(:x) | ||
assert_equal @dimensions[2], @imperial_package.inches(:max) | ||
assert_equal @dimensions[2], @imperial_package.inches(:length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had no idea this was possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make it impossible ok? And make a more normal looking API.
assert_in_delta 17.2, @package.centimetres(:girth), 1 | ||
assert_in_delta 17.2, @package.centimetres(:around), 1 | ||
assert_in_delta 17.2, @package.centimetres(:circumference), 1 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra 🌠
d0e2c6e
to
bb56c2a
Compare
Split up and test Package and PackageItem
What's the deal with
Package
andPackageItem
Problem
These classes are a bit of a mess. The APIs are not clear, they don't do what you think they'd do, they have implicit behaviour, they're mostly untested, and we have no confidence in changing them.
Solution
Split the class out into its own file. Test the specs as they exist.
There are even a bunch of cases where calling them with valid APIs have bugs and they raise. I'm not changing any behaviour. Just asserting the raises and skipping the tests. If the tests stop raising they'll then fail.
This lets us pull out
Quantified
and replace withmeasured
as in #434. This is really just a first step in that refactor.Note: I wrote so many tests you may have to click
View Diff
because the last file with 400+ lines is collapsed.@jonathankwok @mdking @thegedge