Advice on writing Ruby tests for plugins

One can write tests for the backend of a plugin. For example, I created the following file in my plugin directory:

spec/lib/route_store_spec.rb:

require 'rails_helper'

describe MyPlugin::RouteStore do
  describe ".get_routes" do
    it "should return a Hash" do
      expect(described_class.get_routes().is_a?(Hash)).to be true
    end
  end
end

And then, I run the tests like this from Discourse’s root directory:

bundle exec rake plugin:spec["my-plugin-dir"]

/!\ Update: Should you get the error zsh: no matches found: plugin:spec[my-plugin-dir], that’s a known issue with zsh. You can workaround it by running the tests like this instead:

bundle exec rake "plugin:spec[my-plugin-dir]"
Old content before adding the update note

First of all, the following for running the tests don’t work although I see it recommended as the way to run the tests in the forum a lot:

  • bundle exec rake plugin:spec["disraptor"]: Errors with

    zsh: no matches found: plugin:spec[disraptor]
    
  • bundle exec rake plugin:spec disraptor: errors on a different plugin’s tests with:

    Failure/Error: class DetectedLink < Struct.new(:url, :is_quote); end
    
    TypeError:
      superclass mismatch for class DetectedLink
    

Now, I have a general question regarding testability of certain code. The method I want to test in the code above looks like this:

described_class.get_routes():

def get_routes
  return PluginStore.get(MyPlugin::PLUGIN_NAME, 'key') || {}
end

Both parameters of PluginStore.get are hard-coded and currently cannot be changed from within the tests. This makes the method untestable because I would need to test actual plugin data from the store which I cannot rely on being there.

Discourse’s own tests for the discourse-narrative-bot plugin work around this by making the key parameter a parameter in the method they want to test (see plugins/discourse-narrative-bot/spec/discourse_narrative_bot/store_spec.rb for reference).

Are there other options for testing a plugin’s store?

1 Like

You need to use the directory name for you plugin here. So if it’s under /plugins/discourse-disraptor, then you run bundle exec rake plugin:spec[discourse-distraptor]

Why not just put the data in the database? Before testing the get_routes function, run this:

PluginStore.set(MyPlugin::PLUGIN_NAME, 'key', 'valuefortest')

The database is cleaned up automatically after each individual test. (tests run in a DB transaction, which is rolled back automatically)

5 Likes

I do. That’s the plugin’s name and directory name. That command errors as stated.

1 Like

Interesting… I guess it must be failing on this line

https://github.com/discourse/discourse/blob/master/lib/tasks/plugin.rake#L90

To confirm, your spec file is located at /plugins/disraptor/spec/lib/route_store_spec.rb?

How is your development environment configured? Windows / Mac / Linux?

2 Likes

That’s correct. However, disraptor is a symbolic link. Could that be the culprit? I’m using Ubuntu.

Also, note how the error that is produced there reads “No specs found.”, but I am getting “zsh: no matches found”.

1 Like

I think you’re right

Using symlinks for plugins in development seems like a pretty reasonable setup, so I went ahead and added support in https://github.com/discourse/discourse/commit/dfd63b185f3b5279f49f979638dd17c30431669e

4 Likes

I just tried running bundle exec rake plugin:spec["poll"] and that produces the same error. However, the poll plugin is not symlinked, so that can’t be quite right, or can it?

The piece of code you linked to is also not executed when running bundle exec rake plugin:spec["poll"].

Comparing with the other command: bundle exec rake plugin:spec poll produces the same error in the unrelated plugin as stated in the original post.

1 Like

To be sure we’re looking at the correct part of the code, can you try running rails c and then

Dir.glob("./plugins/poll/spec/**/*_spec.rb")
1 Like

That produces the correct output:

[1] pry(main)> Dir.glob("./plugins/poll/spec/**/*_spec.rb")
=> ["./plugins/poll/spec/lib/polls_validator_spec.rb",
 "./plugins/poll/spec/lib/polls_updater_spec.rb",
 "./plugins/poll/spec/lib/pretty_text_spec.rb",
 "./plugins/poll/spec/lib/new_post_manager_spec.rb",
 "./plugins/poll/spec/db/post_migrate/migrate_polls_data_spec.rb",
 "./plugins/poll/spec/integration/poll_endpoints_spec.rb",
 "./plugins/poll/spec/controllers/posts_controller_spec.rb",
 "./plugins/poll/spec/controllers/polls_controller_spec.rb",
 "./plugins/poll/spec/requests/users_controller_spec.rb",
 "./plugins/poll/spec/jobs/regular/close_poll_spec.rb"]

Same for my plugin, by the way:

[2] pry(main)> Dir.glob("./plugins/disraptor/spec/**/*_spec.rb")
=> ["./plugins/disraptor/spec/lib/route_store_spec.rb"]
1 Like

How about

bundle exec rake "plugin:spec[poll]"

That should stop zsh trying to be clever

2 Likes

That’s it! I updated the original post to include this information. You might also want to revert the change in https://github.com/discourse/discourse/commit/dfd63b185f3b5279f49f979638dd17c30431669e as traversal of symbolic links works already, right?


Regarding the testability question, I will clear the database in the tests which will not mess with the database from the development environment because all transactions will be rolled back.

1 Like

This should all be handled automatically. The test environment uses a separate database, and each test is run inside a transaction (which is never committed to the DB).

3 Likes

Ah, thank your for clearing that up. My RAILS_ENV was set to development, so the tests were running in development mode, too. After unsetting that, it uses the test environment by default. If one wants to enforce a test environment for the tests, they can run this:

RAILS_ENV=test bundle exec rake plugin:spec["my-plugin-dir"]
3 Likes

bin/rake autospec stops a lot of this fuffing around.

Simply run that … then save a spec file in your plugin.

3 Likes

FYI I fixed symlink traversal in bin/rake autospec today which was somewhat tricky but totally worth it.

https://github.com/discourse/discourse/commit/3b77fb1fb1c5e42c5ee07a97ae4aee49f0edf12f

6 Likes