Based on Sandi Metz's very precise explanations, I will try to give you the ultimate mnemonic (if you learn it) or cheatsheet (if you bookmark/copy it) to the question what to test?.
First, let's establish the terms I will use to describe the four options:
Query: is a method that returns data but does not change the state of the receiver object, i.e.:
>> a = [1,2,3] => [1, 2, 3] a.size => 3 >> a => [1, 2, 3]
Command: is a method that may or may not return a value, but it always changes the state of the receiver object.
>> a = [1,2,3] => [1, 2, 3] >> a.pop => 3 >> a => [1, 2]
Incoming: is a method defined in the class you are testing, and which behaviour you want to test on its unit test.
Outgoing: is a method defined out of the class you are testing, and which behaviour you need to simulate but don't want to test on your unit test.
Ok, those are the definitions, now for the rules:
- Assert the return value of the method
- Assert the return value of the method (if there is one)
- Assert the change in state
You changed state might not be accesible via the public interface of your object, but as Xavier Noria says, if it's important enough to need a text, then it's worth using
instance_variable_get() to check on it. Take advantage of ruby's advanced introspection capabilities.
- Stub the response of the other class
The gist here is, you need some value provided by some external class but that class does not mind if you call it or not, as it states remains unchanged.
So, for your test, what you need is that value, and not the class that provides it, especially if it's not-yours, complicated to set up for answering a particular value, not under your control, etc.
This is how you stub a method call with Minitest's included stub system:
OtherKass.stub :query, "response" do assert_equal "expected_value", obj_under_test.tested_method end
- Stub the response of the other class (if you need it)
- Mock the receiver object to check that the call is made
Sometimes, the call you make on other class has side-effects, especially intended side-effects, like persistance, notifications, money-transfers, asset creation...
If this a unit test, how the other class manages it's state is none of your business, what you have to test is that the call is made with the right arguments.
It is the job of the other class tests to check your call as an "incoming command"
The toll Minitest provides us with for this predicament is the
Minitest::Mock class. I has two methods of interest:
.expect(...): what method and arguments must we verify (called before the exercise of the method under test)
.verify: Did the expected calls happened as specified? (called after you call your object's method, to see that it triggered the expected external call)
Let's see an example:
# setup @mock = Minitest::Mock.new @mock.expect(:outgoing_command, "return_value", [String]) # exercise within your class @mock.outgoing_command("foo") # => true # verify at the end @mock.verify # => true
There are more elaborate details about how to use
.expect on the documentation
The complex thing is usually to get your class to use the mock, which can be done via a previous stub, or dependency injection:
This is quite common, and not bad if you only have one external dependency with a very stable interface, but if you see yourself having several of these per test, try to decouple your classes and inject your dependencies like the example after this one.
Example taken from Mocking in Ruby with Minitest
require 'test_helper' class UserTest < ActiveSupport::TestCase test '#apply_subscription' do mock = Minitest::Mock.new mock.expect :apply, true SubscriptionService.stub :new, mock do user = users(:one) assert user.apply_subscription end assert_mock mock # New in Minitest 5.9.0 assert mock.verify # Old way of verifying mocks end end
This pattern is not only easier to test, it is also easier to refactor when needing to change collaborator objects, maybe not at the same time for all usages, think Adapter Pattern or feature flag or maybe A/B-test selected strategies.
class UserGreeter def initialize(name:, notifier: Slack) @name = name @notifier = notifier # store replaceable collaborator object end def run msg = whatever(@name) @notifier.notify(msg) # use injected dependency by internal name end end # on the test require 'test_helper' class UserGreeterTest < ActiveSupport::TestCase test '#run' do mock = Minitest::Mock.new mock.expect :notify, true, [String] user = User.new( user_attrs.merge(subscription_service: mock) ) assert user.run assert_mock mock end end
And here as promised I leave you tha handy mnemonic table: