Reputation: 1319
First of all I know it is bad practice to have multiple assertions in unit test.
But sometimes you need to test some atomic transaction. As a simplified example, let's take some banking application that has the Account class:
class Account
attr_accessor :balance
def transfer(to_account, amount)
self.balance -= amount
to_account.balance += amount
Audit.create(message: "Transferred #{amount} from #{self.number} to #{to_account.number}."
end
end
In this situation I want to check 3 things together:
amount
amount
What is the best way to test the @account.transfer
method?
Upvotes: 2
Views: 1369
Reputation: 47548
In this situation I want to check 3 things together:
I'd argue that what you really want is to describe the behavior of these things under certain conditions, and thus ensure that the behavior meets your specifications. That might mean things happen together; or it might mean that some things only happen in one set of conditions and not others, or that an exception causes everything to be rolled back to its original state.
There is no magic to having all your assertions in one test, except to make things faster. Unless you are facing a severe performance penalty (as often happens in full-stack tests), it is much better to use one assertion per test.
RSpec makes it straightforward to extract the test setup phase so that it is repeated for each example:
class Account
attr_accessor :balance
def transfer(to_account, amount)
self.debit!(amount)
to_account.credit!(amount)
Audit.create!(message: "Transferred #{amount} from #{self.number} to #{to_account.number}."
rescue SomethingBadError
# undo all of our hard work
end
end
describe Account do
context "when a transfer is made to another account" do
let(:other_account} { other_account }
context "and the subject account has sufficient funds" do
subject { account_with_beaucoup_bucks }
it "debits the subject account"
it "credits the other account"
it "creates an Audit entry"
end
context "and the subject account is overdrawn" do
subject { overdrawn_account }
it "does not debit the subject account"
it "does not credit the other account"
it "creates an Audit entry" # to show the attempted transfer failed
end
end
end
If all three tests in the "happy path" passed, then they all "happened together", since the initial system state was the same in each case.
But you also need to ensure that things don't happen when something goes wrong, and that the system goes back to its original state. Having multiple assertions makes it easy to see that this works as expected, and when tests fail, exactly how they failed.
Upvotes: 2
Reputation: 3407
Multiple assertions per test is not always a bad practice. If the multiple asserts verify the same behaviour, there's no problem with it. The problem exists when trying to verify more than one behaviour in the same test. Of course there are some risks with multiple asserts per test. One of them is you may accidentally leave values from a previous test set that invalidates a previous test in a weird way. Also, when one assert is false, all the other left will not be executed, which can cause difficulties to understand what's goin on. But be reasonable, you can have multiple asserts asserting the same behaviour, preferrably short ones and with no extra setup.
In the simple case you brought, I would use multiple asserts, because it is so simple. But of course it can get a lot more complicated, like negative balance, different types of accounts and stuff. Then it would be better to use different tests with one (preferrably) assert. I would organize it like this:
1 to test Audit in every of these possibilities;
1 to test the behaviour of the current to_account (simplest case);
Since the Audit test is pretty simple and requires no extra setup, you can also test it along with Account and to_account.
Upvotes: 1