8

Testing external dependencies using dependency injection

 3 years ago
source link: https://sourcediving.com/testing-external-dependencies-using-dependency-injection-ad06496d8cb6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Testing external dependencies using dependency injection

Image for post
Image for post

If you work on a bigger Ruby (on Rails) project, chances are that you need to interact with some external resource like a database, cloud API or message queue. Testing these external resources can sometimes be tricky, let’s have a look at different approaches.

Example

As an example, we will use the Dropbox API gem to upload a file.

We have an Uploader class which accepts the file content and file name. Finally it uses the Dropbox client to upload the data to the cloud.

WebMock

An approach we often see is to use WebMock to stub the HTTP requests.

From the official documentation, it is described as a

Library for stubbing and setting expectations on HTTP requests in Ruby.

Our test code could look like this

With this approach, we test that the correct Dropbox API URLs are called. This has the advantage that we can refactor our code and even use a different library and as long as we still call the same URLs this test would pass.

However, if Dropbox decides to update their API we would need to change our specs. If we’re treating the gem as our public interface, and trusting it to be maintained and respond to API changes, then having to update those specs every time seems like an unnecessary step. We could argue our test might have too much low-level details and this should get tested in the gem instead.

PORO fake

An alternative could be to build our own Plain Old Ruby Object (PORO) fake.

In case we want to swap out Dropbox for another cloud storage, we would need to change the Uploader class (open-closed principle). Instead, Uploader should accept a client parameter which is called dependency injection.

As we now only need to provide an object which responds to upload_by_chunks we can implement a simple test client fake too.

The cattr_accessor is a handy helper to create a getter and setter for a class attribute. In our upload_by_chunks method, we just store the parameters in this class attribute. We can now use this test client in our specs like this.

However, our Uploader class is now tied to the public API of the Dropbox gem. If the upload_by_chunks method is renamed for example, the class would have to be modified even though from its perspective nothing has changed.

A way to avoiding this is sometimes referred to as the “dependency inversion principle”, that suggests to “depend upon abstractions — not concretions”. We can fix this by wrapping our DropboxApi::Client into a DropboxClient adapter with a common ‘interface’ upload.

With this change we also make the public interface of the uploader client smaller (interface segregation) and easier to support more clients (e.g. Box) in the future as we only need to write a new adapter.

We don’t couple our specs to the Dropbox API or gem anymore as we test against our adapter interface. As always, there is however a trade-off: We need to maintain our PORO fake and keep it in sync with our adapter and specs might still pass even though the Dropbox gem or API changed. We should test our wrapper class too.

Summary

By using dependency injection with a PORO fake, our specs and implementation not only became more extensible but also more robust against changes. It might not be needed for every situation, but used at the right moment can be a useful tool.

Some examples where we use this pattern at Cookpad include sending SMS messages, Kafka messages, push notifications and even Redis.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK