I am writing an app that periodically fetches data from a third-party API. The data is for a rails model
and to simplify we’ll say that it is contacts being fetched from a (fictional) provider called
Contactio. I’ve got these classes:
-
ContactioWrapper
(class in lib)
Wraps Contactio’s API, providing methods such ascontacts
andcontact
. Gets XML from Contactio and converts it to hashes for ease-of-use internally. -
Contact
(Rails model)
Providesupdate_or_create_from_hash
which takes a hash fromContactioWrapper
, converts it to a model and saves it. -
SynchronizesWithContactio
(class in lib)
UsesContactioWrapper
to fetch information and tellsContact
to update/create the models.
Testing the first two seems straight-forward:
ContactioWrapper
: Given a known XML, does it return the expected hash?
Contact
: Given a known hash, does it convert to and save the expected model?
But I am not sure how I want to test SynchronizeWithContactio
. Maybe one of these:
1. Black box test (verify input and output)
Given a certain XML, does the correct model get saved?
it "saves contact successfully" do
stub_contactio method: :contact, xml: some_known_xml # could use vcr
contact_id = <ID from XML>
synchronize_with_contactio.contact contact_id
contact = Contact.find contact_id
expect(contact.name).to equal "<Name from XML>"
end
2. Tied to implementation
Expect that SynchronizeWithContactio
calls certain methods on ContactioWrapper
and Contact
:
it "fetches contact from Contactio and tells Contact to update it" do
contact_id = double
expected_hash = double
expect(contactio_wrapper).to receive(:contact).with(contact_id).and_return(expected_hash)
expect(Contact).to receive(:update_or_create_from_hash).with(expected_hash)
synchronize_with_contactio.contact contact_id
end
Thoughts on #1
Pros:
- Implementation can change without breaking tests
- An integration test is needed anyway and this would be it
Cons:
- Tests more than it should (the responsibility of
SynchronizeWithContactio
is to orchestrate - it doesn’t know anything about XML, why is that part of the setup?) - Doesn’t give much insight when it breaks
Thoughts on #2
Pros:
- Only verifies and knows about what it actually should
Cons:
- Fragile
- Very close to being a duplication of the implementation (especially if there are only a few code paths)
Please provide your feedback on the tests and thoughts above. What do you think about it all?