Skip to content
Jonathan Rochkind edited this page Sep 1, 2015 · 29 revisions

So you want to write code in Umlaut gem itself.

When to write Umlaut code

First, note that you may not have to for local customization. The most common thing you'd want to do is add a new Service Plugin. You can add a new service plugin solely in your local app, and reference it in your umlaut_services.yml. However, if it's possibly useful to someone else, it would be great if you contributed it back to Umlaut, and that would require editing Umlaut source. See also Writing a Service Plugin

Or you may just want to explore the source and maybe even edit it experimentally, that's cool.

Please note: You can edit Umlaut code to contribute a patch back to Umlaut, or experimentally. But you should not need to run your application live against locally edited Umlaut source code. If you do so, you have locally forked Umlaut, and it will make upgrading to future versions of Umlaut difficult. It should be possible to make any local changes you need with local configuration or over-rides located in your local app, but still using the shared Umlaut gem. If there's something that needs to be easier or which you can't figure out how to change locally... well, that's indeed a reason to fix Umlaut to make it easier and contribute your fix back to Umlaut! Please feel free to contact us for questions or guidance.

Local copy of Umlaut

So ordinarily when you install Umlaut into a local app, the actual Umlaut source is off in your system (or rvm, etc) gem repository, which is not a place you'd want to edit it.

You can use git to check out a copy of Umlaut source wherever you want from your github repo. But Umlaut's a gem, you can't actually run it to test it. How do you run an application against an arbitrary local umlaut version in your file system?

Two ways:

  • In any arbitrary umlaut-using local app, you can simply point it to a local checkout of Umlaut in the app's Gemfile, using the :path option. Relative paths are fine.

    gem 'umlaut', :path => '../umlaut'
    

    You wouldn't want to actually deploy or run an app in production like this, it's best to deploy against actual released versions of Umlaut. But for testing and experimentation and development.

  • The umlaut source comes with a very basic dummy app in ./test/dummy. There's no database.yml though. Create one, pointing to a local mysql database, run rake db:migrate, and you can just start the dummy app running against the checkout of umlaut in the directory that contains it. Edit the ./config/umlaut_services.yml if you want, etc.

    This dummy app was actually created by the rails plugin new generator for purpose of running automated tests against, see below.

Coding standards

  • Please, straight javascript, no coffeescript.
  • scss is great though

(Automated) Testing

Automated testing is challenging in umlaut. Because of Umlaut's legacy architecture originally written without tests (Umlaut is over 8 years old!); because testing in Rails is hard to begin with, especially Rails engines; because Umlaut's use of multi-threaded concurrency sometimes causes additional problems for Rails' testing assumptions; and Umlaut's extensive use of external HTTP API's doesn't help.

But test coverage has gotten pretty decent, and we're working to improve test coverage and tools to make it easy to write tests. In general, any new code committed should come with tests.

Running Umlaut Tests

Umlaut doesn't work against the sqlite3 database, it requires MySQL (or experimentally Postgres). So you need to create a MySQL (or experimentally Postgres) database to run the tests against, in order to run the tests. It should not be the same database you use for any production activities, it will be altered and sometimes wiped out by the tests.

  1. Create or identify MySQL user, and decide what database name you will use.
  2. In Umlaut source, copy ./test/dummy/config/database.yml.example to ./test/dummy/config/database.yml. Fill out your new copy of database.yml with your database details.
  3. Create the database in MySQL, or, from the Umlaut source code directory, run RAILS_ENV=test bundle exec rake db:create to create the database you specified in database.yml.
  4. Run RAILS_ENV=test bundle exec rake db:migrate to set up the database.
  5. Run bundle exec rake test to run tests.

You can run a single test file with:

 bundle exec ruby -Ilib:test ./test/path/to/test_file.rb

You can try to run a single test in a file by using the -n argument standard ruby test:unit, with a regex matching the name of one or more tests. It sort of usually works. Eg:

 bundle exec ruby -Ilib:test test/unit/scopus2_test.rb -n /non_doi_query/

Writing Umlaut tests

Advice and standards for writing tests in Umlaut itself; the same advice can largely be applied to writing tests in a local application or plugin gem that uses Umlaut functionality as well, and we'll touch on that where appropriate.

Umlaut's own tests are written with standard test-unit-style tests, as demo'd in the Rails Guide to Testing. Getting testing to work with Umlaut was hard enough for us, it seemed safest to avoid fighting with a third-party tool and just get the tests out there. And because jrochkind prefers assertion-style to expectation-style (although now you can do assertion-style with minitest-spec and even with rspec).

In your own code in a local app or plugin that uses Umlaut, you could try using minitest-spec or even rspec, but you'll be on your own adapting this advice to those tools.

Use VCR to record third party HTTP transactions

A lot of Umlaut functionality interacts with third party HTTP API's. We use vcr to 'record' these transactions in the tests, and tests can be played back based on recorded transactions without actually contacting the remote servers. This allows tests to be run quicker without needing to contact the servers live; it also allows tests to be run by people who don't have API keys for the third party API's tested.

Umlaut provides the TestWithCassette mix-in and helper method to make it easy to use VCR with test-unit style:

class GoogleBookSearchTest < ActiveSupport::TestCase  
  extend TestWithCassette

  # Create a test wrapped in VCR recording, to provide a deterministic GBS search. 
  test_with_cassette("frankenstein by OCLC number", :google_book_search) do
    hashified_response = @gbs_default.do_query('OCLC2364071', requests(:frankenstein))
    assert_not_nil hashified_response
    assert_not_nil hashified_response["totalItems"]
    assert_operator hashified_response["totalItems"], :>, 0 
  end
end

If the test needs an API key, you should use VCR's filter_sensitive_data feature to keep the API key out of the recorded transactions which end up in the git repo. Here's how we do it:

class Scopus2Test < ActiveSupport::TestCase
   # Set @@api_key class var to environmental variable if present, otherwise
   # dummy value. When running without recorded VCR cassette, you'll need ENV var. 
   @@api_key = (ENV["SCOPUS_KEY"] || "DUMMY_API_KEY")
   # Tell VCR to keep the API key, which might be real, from being recorded     
   VCR.configure do |c|
     c.filter_sensitive_data("DUMMY_API_KEY", :scopus) { @@api_key }
   end
   # ...
   # Now when writing code that needs an API key, use the @@api_key var...
   service = Scopus2.new('service_id' => 'test_scopus2', 'api_key' => @@api_key, 'priority' => 0)
   # ...
end

All tests committed to Umlaut should pass for anyone, locally, without needing an API key. Usually by using VCR.

To run a previously recorded test against the remote server(s) live, just delete the appropriate one or more VCR cassettes in ./test/cassettes, and re-run tests. Commit the changed cassettes if desired.

You have access to TestWithCassette in an app or gem that depends on Umlaut, just require 'umlaut/test_help' and it comes with. See below.

Umlaut::TestHelp helper methods

require 'umlaut/test_help'
include Umlaut::TestHelp

The Umlaut::TestHelp module has some convenience methods to make testing Umlaut easier. It is available to you in Umlaut tests, and can be require'd and include'd into your tests in an app or gem that depends on umlaut.

  • umlaut_request = fake_umlaut_request("au=Smith&title=Foo&etc")
  • with_service_config lets you wrap a test in a temporary specificied umlaut_services.yml hash
  • assert_dispatched -- assert that a particular request has a dispatch recorded for a certain service, in a certain dispatch state
  • assert_service_responses -- assert that service responses matching certain criteria have been recorded on a given request

Maybe more in the future. The TestWithCassette module is defined in this file too, so you get it when you require this file in an app or gem that depends on Umlaut.

Avoid fixtures

It's better to avoid the Rails 'fixtures' function. Just create the ActiveRecord objects you need for a test, the ordinary way. See Umlaut::TestHelp for some helpers there.

Disable Transactional fixtures sometimes

Rails "transactional fixtures" doesn't always play well with Umlaut's multi-threaded concurrency. Contrary to the implication of the name, it effects more than just fixtures, it makes Rails try to use a separate db transaction for each test in general, which does odd things if your test winds up trigger multi-threaded concurrency in Umlaut.

It's best to turn off transactional fixtures in your tests, especially if you are seeing odd unreproducible issues. We probably should turn it off globally in Umlaut but haven't yet.

Wait for ResolveController bg threads

If you are testing the ResolveController#index method, with a service configuration involving background waves, then the action launches some threads that keep going even after the action completes.

This means those threads might keep going even after the test ends, which can lead to extreme weirdness.

Either use a service configuration with no background threads, or in any test that may be subject to this, call @controller.bg_thread.wait before the end of the test, to wait on all bg threads to complete. The bg_thread method in ResolveController is pretty much just there for this case of supporting testing.

Note on the dummy app

We used rails 3.1 rails plugin new generator to create a gem plugin skeleton for Umlaut.

One thing this did was create a ./test/dummy dummy app in umlaut source. This is meant for running tests against. Tests you write in /test/* will run against the dummy app. We've manually upgraded this dummy app from Rails 3.1 to current latest version tested against.

Running tests against other Rails versions

We only have one dummy app in source, however by loading different versions of Rails in the gemfile, we can try to run tests against different versions of Rails. We have a line in the local Gemfile that looks at a RAILS_GEM_SPEC env variable, and if found will insist on that version of Rails. This is somewhat experimental and clunky, but you can try:

$ RAILS_GEM_SPEC="~> 3.2.0" bundle update
$ RAILS_GEM_SPEC="~> 3.2.0" bundle exec rake test

When you are done, you'll probably want to restore your Gemfile.lock to latest version of Rails allowed with $ bundle update (no RAILS_GEM_SPEC env var).

We do not currently run against multiple versions of Rails in travis CI, only against the latest allowed with Umlaut.

Test should pass on Travis, too

Travis is a continuous integration service for the open source community that Umlaut leverages to run tests. One reason to use Test::Unit/Minitest is that Travis will automatically run your tests.

Travis creates the necessary MySQL databases and runs tests against the dummy app based on the database configuration in ./test/dummy/config/travis_database.yml.

One specific test Travis runs is for SFX search functionality.
Travis uses the configuration in ./test/dummy/config/travis_database.yml to create a mock instance of an SFX global and local database, migrates the SFX database schema and the test populates the database with deterministic SFX Object and AZ Title fixtures.
It tests basic search functionality and serves as a sanity check to make sure SFX searching is working as expected. This test make @jrochkind very nervous, and you probably shouldn't ever run this test locally. The migrations and test code only run in the case that the SFX database configuration is specified as a 'mock instance' in order to prevent overwriting a real SFX database. (Yet another reason the Umlaut user that queries SFX should only have read permissions.)

Release a service plugin in a seperate gem?

Umlaut comes with a bunch of service plugins included.

You/we could instead release one or more service plugins as seperate gems, that depend on Umlaut. A user would need to install the umlaut_crazy_service gem, and then they'd have access to service plugin same as anything else. No special magic, the seperate gem just needs to depend on umlaut in it's gemspec, and include a class defining a service per Umlaut conventions.

Why might you want to do this?

  • The service has a lot of dependencies that we don't want core umlaut to need to depend on
  • You want to be able to release updates to the service plugin on it's own release schedule, not tied to updates of umlaut.
  • You want to control the service yourself and do whatever you want with it, not subject to other umlaut developers ideas. Or you want/need to release it under a different license.

What are the downsides?

  • A bit more apparatus to work with as a developer, a bit more confusing as a developer (not so much as a user), a bit more work to make sure the independent gem keeps working with Umlaut as umlaut evolves, keep track of what versions of Umlaut your independent gem works with, etc.

Submitting a change

So you're not an Umlaut committer, but you'd like to submit a change back to Umlaut.

  • Fork in github, create a new "feature" branch in your forked github, and commit your changes there.

  • Then submit a github pull request back to Umlaut. See also http://www.mikeball.us/blog/how-to-contribute-to-a-project-hosted-on-github

  • Small single purpose pull requests are better than massive conglomerations. If you need massive changes, try to break them into steps and submit pull requests for smaller changes at a time.

  • Please feel free to ask for feedback on the umlaut listserv on your general approach before embarking on a massive change, to make sure you're working in a direction likely to be accepted.

Once you've contributed a quality change or two to Umlaut, odds are we'll make you a committer -- but the pull request process should still be used for code review of anything significant.

Releasing an Umlaut gem

Generally only done by project lead or what have you. Needs to have permissions set on rubygems for the umlaut gem.

We use the bundler-supplied rake tasks for releasing the gem. Update ./lib/umlaut/version.rb to desired version.

Then simply run: rake release

This will create a tag with version in umlaut git repo, as well as release the new version to rubygems.org.