Skip to content
This repository has been archived by the owner on Jun 13, 2018. It is now read-only.

Split up and test Package and PackageItem #435

Merged
merged 1 commit into from
Dec 7, 2016

Conversation

kmcphillips
Copy link
Member

What's the deal with Package and PackageItem

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 with measured 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

)
end

def test_accessors
Copy link
Member Author

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
Copy link
Member Author

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
Copy link
Member Author

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.

@kmcphillips
Copy link
Member Author

A first step toward #427 in 2.0

Copy link
Contributor

@thegedge thegedge left a 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
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

@thegedge thegedge Dec 6, 2016

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_readers to a manual def attr; ...stuff...; end in the future.

Sorry for the wordiness, but hopefully that cleared up my stance :)

Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

@thegedge thegedge Dec 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of things:

  1. 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".
  2. 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!

Copy link
Member Author

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.

Copy link
Contributor

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
Copy link
Contributor

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?
Copy link
Contributor

@thegedge thegedge Dec 6, 2016

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)
Copy link
Contributor

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
Copy link
Contributor

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...

Copy link
Contributor

@jonathankwok jonathankwok left a 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)
Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra 🌠

@kmcphillips kmcphillips force-pushed the package-refactor-test branch from d0e2c6e to bb56c2a Compare December 6, 2016 19:48
@kmcphillips kmcphillips merged commit aaca900 into master Dec 7, 2016
@kmcphillips kmcphillips deployed to rubygems-1-9-stable January 30, 2017 18:52 Active
@maartenvg maartenvg deleted the package-refactor-test branch September 18, 2017 15:54
maartenvg pushed a commit that referenced this pull request Nov 9, 2017
Split up and test Package and PackageItem
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants