开发者

Unit tests for 3rd-party libraries

I'm working on my first real-world Rails project. So far, I'm mostly mashing up functionality from 3rd-party libraries. There's a lot of great ones out there, and that's great.

I'm wondering about whether writing unit tests around these libraries is necessary/useful or not. For example, I just integrated friendly_id around one of my models. Besides installing the gem and adding it as a project dependency, this extent of this integration amounted to:

has_friendly_id :name

It Just Worked, and I barely consider this to be "code I wrote". So what should I be writing by way of tests?

There's two caveats to my question:

  1. I'm assuming that all of my 3rd-party libraries have appropriate tests of their own -- and so writing new unit tests directly against those libraries looks like repeating code. (If I had to use a poorly-tested library then I'd be less hesitant to write tests for it.)
  2. Under Defect-Driven-Testing, I'd definitely write a test the second I encounter a probl开发者_Go百科em. If the test uncovered a bug in the library, then I'd probably submit the test to the maintainer.

Outside of that though... is there much point to testing 3rd-party code?


Never trust third-party libraries. I always write a set of tests I call "sanity checks" for third party libraries, but I only apply it to one model/controller/whatever. So for example if I'm using the acts_as_paranoid gem (which makes database records look deleted but not actually delete them) I'll write a set of tests for just one of the models that uses the extension, and assume it works for the rest of them. This lets me sleep at night knowing that I can update the gem when new releases come out, or even use the "edge" version, and the expected functionality is going to be reliable.


It is not the normal practice to unit test some other party's code. Normally, you would trust your upstream dependencies to work correctly.

But that's assuming you actually do trust them. There are all sorts of reasons this should break down.

For one thing, you are stuck with a dependency that is approximately abandoned, with a decent smattering of bugs. As you discover the bugs, write tests that exercise the bugs and exercise workarounds for the bugs.

Another reason might be because the third party keeps changing every damn thing. As you can make time, it's reasonable to add tests for dusty corners that you actually use, because those are the most likely to change on you in a new version.

Obviously either of these cases are really a huge waste of your precious time you could be spending making your app better rather than dealing with something outside your control. If you find you are in need of this kind of testing, you should really be looking for a more trustworthy alternative to that particular dependency.


Here are my opinions, based on experience:

  • Don't test third party libraries in your web app. Fork them, make sure they're unit tested within themselves, and depend on your fork (or a version or commit or tag) until you're ready to upgrade to the next version. If you find bugs, make the changes on your fork and send a pull request to the official repository. Until the bugs are pulled into main line, use your own fork.
  • Making an adapter layer (module or class that calls the 3rd party service) is a great idea, but better suited for libraries, or gems than web app codebases. One nice thing about adapters, is that you can make a Mock version of them to be loaded in the testing environment only.
  • Mock out external http calls using WebMock or FakeWeb or Artifice or... and the list goes on. Make sure you tell the utility not to allow external calls in the test environment.
  • Maybe use VCR to record and mock out all http requests on subsequent test runs (One warning about VCR--it's very useful, but be wary of using its automatic cassette naming strategy with a larger codebase--any app-wide changes will render most of your auto-generated cassettes useless. If you instead simply use VCR.use_cassette('cassete-name') do blocks, it should be very similar to using WebMock/Fakeweb/Artifice, etc. The latter is my preference.)


No, you shouldn't test 3rd party code, where possible it should sit behind an interface so you can swap out the functionality easily if needs be.
The interface would allow you to use mocks in your code, to mock out calls to the libraries.


Do not write unit tests, write integration instead!


A short example for countries gem:

require 'test_helper'

class CountriesLearningTest < ActiveSupport::TestCase
  def test_returns_country_name
    assert_equal 'United States of America', Country.new('US').name
  end

  def test_returns_country_alpha2_code
    assert_equal 'US', Country.new('US').alpha2
  end
end

I think it's safe to assume that unit tests of the library are good enough. However, writing so called leaning (or discovery) integration tests might be beneficial for the following reasons:

  • If the dependency is totally new, it allows you to get more comfortable with the 3rd party API.
  • As any other good test it acts as documentation, so other programmers (including you at a later point) will not need to toy with the library to see how it works.
  • The way the library is used in the project becomes more apparent.
  • It acts as a safety net when updating the library to the major version, which may include incompatible changes.

Another example for paper_trail gem:

require 'test_helper'

class Post < ApplicationRecord
  has_paper_trail
end

class PaperTrailLearningTest < ActiveSupport::TestCase
  setup do
    ActiveRecord::Base.connection.create_table :posts do |t|
      t.string :title

      # Otherwise `touch` fails silently
      t.datetime :updated_at
    end
  end

  def test_returns_version_list
    @record = Post.create!(title: 'any')

    assert_equal 1, @record.versions.count
    assert_respond_to @record.versions.first, :item_id
  end

  def test_creates_new_version_upon_update
    @record = Post.create!(title: 'old title')
    original_record = @record.clone

    assert_difference -> { @record.versions.size } do
      @record.update!(title: 'new title')
    end
    version = @record.versions.last
    assert_equal @record.id, version.item_id
    assert_equal @record.class.name, version.item_type
    assert_equal version.reify, original_record
    assert_equal ['old title', 'new title'], version.object_changes['title']
    assert_equal 'update', version.event
  end
end
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜