Tomo::Testing::MockPluginTester

MockPluginTester is a helper object that allows tasks and helpers provided by plugins to be easily unit tested. It has no test framework dependencies so it can be used in Minitest, RSpec, or the testing framework of your choice.

MockPluginTester works by mocking the underlying SSH connection so that no actual remote SSH scripts are run. By default, the tester will simulate that the script runs successfully (exit status of 0) with empty stdout and stderr. You can then write assertions verifying that the script was run as expected. For example:

require "tomo/testing"

def test_setup_directories
  tester = Tomo::Testing::MockPluginTester.new(settings: { deploy_to: "/app" })
  tester.run_task("core:setup_directories")
  assert_equal("mkdir -p /app /app/releases /app/shared", tester.executed_script)
end

You can change the default mocking behavior by using mock_script_result, like this:

require "tomo/testing"

def setup
  @tester = Tomo::Testing::MockPluginTester.new(
    "bundler",
    "puma",
    settings: {
      current_path: "/app/current",
      puma_control_url: "tcp://127.0.0.1:9293",
      puma_control_token: "test"
    }
  )
end

def test_restart_starts_puma_if_pumactl_fails
  @tester.mock_script_result(/pumactl/, exit_status: 1)
  @tester.run_task("puma:restart")
  assert_equal(
    "cd /app/current && bundle exec puma --daemon "\
    "--control-url tcp://127.0.0.1:9293 --control-token test",
    @tester.executed_scripts.last
  )
end

Every MockPluginTester instance loads a fresh, independent tomo environment, so mocks, plugins, settings, etc. specified in one tester will not affect any other tests.

Note that you must require "tomo/testing" to use MockPluginTester.

Class methods

new(*plugin_names, settings: {}, release: {}) → new_tester

Build a new MockPluginTester that loads the given list of plugin_names. The resulting tester object can be used to simulate any tasks or helpers that are provided by these plugins. Note that the “core” plugin is always loaded implicitly and does not need to be specified.

Any settings that are specified will be applied after the defaults settings provided by the plugins have been defined. These settings can use template strings just like set.

require "tomo/testing"

tester = Tomo::Testing::MockPluginTester.new(
  "bundler",
  "puma",
  settings: {
    current_path: "/app/current",
    puma_control_url: "tcp://127.0.0.1:9293",
    puma_control_token: "test"
  }
)

Any release data specified will be available to the task under test via remote.release.

Instance methods

run_task(task, *args) → nil

Run the given task by its fully qualified name (the namespace is required). Any args, if specified, are passed to the task via settings[:run_args].

Any remote SSH scripts run by the task (e.g. via remote.run) will be mocked according to rules previously supplied to mock_script_result. If a mock result has not been explicitly supplied, the script will use a default mock that returns a successful result with no output.

require "tomo/testing"

tester = Tomo::Testing::MockPluginTester.new("rails")
tester.run_task("rails:log_tail", "-F")
assert_equal(
  "tail -F /var/www/testing/current/log/${RAILS_ENV}.log",
  tester.executed_script
)

call_helper(helper, *args, **kwargs) → obj

Invoke the specified helper method name with the optional positional args and keyword kwargs. Returns the return value of the helper. Remote SSH scripts are mocked as explained in run_task.

require "tomo/testing"

def test_capture_returns_stdout_not_stderr
  tester = Tomo::Testing::MockPluginTester.new
  tester.mock_script_result(stderr: "oh no", stdout: "hello world\n")
  captured = tester.call_helper(:capture, "greet")
  assert_equal("hello world\n", captured)
end

mock_script_result(script=/.*/, stdout: “”, stderr: “”, exit_status: 0) → self

Mock the return value of remote SSH scripts that match the given script. If script is a String, the mock rule will apply only to scripts that match this String exactly. If script is Regexp, the mock rule will apply to any scripts that match that pattern. If script is omitted, the mock rule will apply always.

In this example, any task or helper invoked via this tester that runs readline /app/current will receive the given mock stdout response:

tester.mock_script_result("readlink /app/current", stdout: <<~OUT)
  /app/releases/20190420203028
OUT

Here, any script that includes pumactl will fail with an exit status of 1:

tester.mock_script_result(/pumactl/, exit_status: 1)

This mocks all scripts to fail with exit status of 255 and stderr of “oh no!”:

tester.mock_script_result(exit_status: 255, stderr: "oh no!")

executed_script → String

The remote SSH script that was run by a previous invocation of run_task or call_helper. If no script was run, then nil is returned. If more that one script was run, this will raise a RuntimeError.

executed_scripts → [String]

All remote SSH scripts that were run by a previous invocation of run_task or call_helper. If no script was run, then an empty array is returned.

stdout → String

Everything that was written to stdout during the most recent invocation of run_task or call_helper. If nothing was written, then the empty string is returned.

stderr → String

Everything that was written to stderr during the most recent invocation of run_task or call_helper. If nothing was written, then the empty string is returned.