Skip to content

Commit

Permalink
Add a true integration test that actually executes Bundler (#41)
Browse files Browse the repository at this point in the history
Before, our CLI tests were doing a significant amount of stubbing to
avoid making external calls to Bundler commands. This limited the value
of these tests. It also made them tedious to write, and required
behind-the-scenes knowledge of when and how the lower-level code called
out to Bundler.

In this PR, I refactored the existing CLI tests so that the stubbing is
done at a higher level: at the `Reporter` API. This makes test setup
easier while still allowing us to write tests to cover various CLI edge
cases, like error handing.

For the primary, "happy path" case, I've created a `CLIIntegrationTest`.
This runs the actual `update-interactive` executable via
`Open3.capture3`, so every layer of the code base is exercised. Most
importantly, Bundler commands themselves are actually executed.

The integration test confirms that when a gem is selected to be updated,
the update really happens. It does this by inspecting the `Gemfile.lock`
to see that the gem version was changed as expected.

I also updated the `mocha` configuration to make stubbing more strict
going forward.
  • Loading branch information
mattbrictson authored Aug 20, 2024
1 parent 72075a3 commit 76a584f
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 45 deletions.
65 changes: 22 additions & 43 deletions test/bundle_update_interactive/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ def test_prints_error_in_red_to_stderr_and_exits_with_failure_status
end

def test_returns_if_no_gems_to_update_and_nothing_withheld
empty_report = stub(empty?: true, updatable_gems: {}, withheld_gems: {})
Reporter.expects(:new).returns(mock(generate_report: empty_report))
stub_report(updatable_gems: {}, withheld_gems: {})

stdout, stderr = capture_io do
CLI.new.run(argv: [])
Expand All @@ -39,17 +38,16 @@ def test_returns_if_no_gems_to_update_and_nothing_withheld
end

def test_prints_withheld_gems_and_returns_if_nothing_to_update
stdout, stderr, status = Dir.chdir(File.expand_path("../fixtures", __dir__)) do
VCR.use_cassette("changelog_requests") do
unchanged_lockfile = File.read("Gemfile.lock")
BundlerCommands.expects(:parse_outdated).returns({ "sqlite3" => "2.0.3" })
BundlerCommands.expects(:read_updated_lockfile).returns(unchanged_lockfile)
mock_vulnerable_gems([])

capture_io_and_exit_status do
CLI.new.run(argv: [])
end
end
report = stub_report(
updatable_gems: {},
withheld_gems: {
"sqlite3" => build(:outdated_gem, name: "sqlite3", updated_version: "2.0.3", changelog_uri: nil)
}
)
report.expects(:scan_for_vulnerabilities!)

stdout, stderr, status = capture_io_and_exit_status do
CLI.new.run(argv: [])
end

assert_equal(<<~EXPECTED_STDERR, stderr)
Expand All @@ -64,38 +62,19 @@ def test_prints_withheld_gems_and_returns_if_nothing_to_update
assert_nil(status)
end

def test_shows_withheld_gems_and_interactive_list_of_gems_and_updates_the_selected_ones
stdout, stderr, status = Dir.chdir(File.expand_path("../fixtures", __dir__)) do
VCR.use_cassette("changelog_requests") do
updated_lockfile = File.read("Gemfile.lock.updated")
BundlerCommands.expects(:parse_outdated).returns({ "sqlite3" => "2.0.3" })
BundlerCommands.expects(:read_updated_lockfile).returns(updated_lockfile)
BundlerCommands.expects(:update_gems_conservatively).with("addressable", "bigdecimal", "builder")
mock_vulnerable_gems([])

stdin_data = " j j \n" # SPACE,DOWN,SPACE,DOWN,SPACE,ENTER selects first three gems to update
capture_io_and_exit_status(stdin_data: stdin_data) do
CLI.new.run(argv: [])
end
end
end
private

assert_equal(<<~EXPECTED_STDERR, stderr)
Resolving latest gem versions...
Checking for security vulnerabilities...
Finding changelogs..................
EXPECTED_STDERR

menu, selected_gems = stdout.split("Updating the following gems.")
def stub_report(withheld_gems: {}, updatable_gems: {})
report = Report.new(
current_lockfile: nil,
withheld_gems: withheld_gems,
updatable_gems: updatable_gems
)

assert_match(/The following gems are being held back and cannot be updated/, menu)
assert_match(/sqlite3.*2\.0\.3/, menu)

assert_equal(3, selected_gems.lines.grep(/→/).count)
assert_match(/addressable/, selected_gems)
assert_match(/bigdecimal/, selected_gems)
assert_match(/builder/, selected_gems)
assert_nil(status)
reporter = Reporter.new
reporter.stubs(:generate_report).returns(report)
Reporter.stubs(:new).returns(reporter)
report
end
end
end
2 changes: 1 addition & 1 deletion test/factories/outdated_gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
FactoryBot.define do
factory :outdated_gem, class: "BundleUpdateInteractive::OutdatedGem" do
current_git_version { nil }
current_version { "7.0.3" }
current_version { "0.0.1" }
git_source_uri { nil }
name { "rails" }
rubygems_source { true }
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/integration/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

source "https://rubygems.org"
gem "bigdecimal"
gem "minitest", "~> 5.0.0"
gem "rake"
18 changes: 18 additions & 0 deletions test/fixtures/integration/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
GEM
remote: https://rubygems.org/
specs:
bigdecimal (3.1.7)
minitest (5.0.0)
rake (12.3.3)

PLATFORMS
arm64-darwin-23
ruby

DEPENDENCIES
bigdecimal
minitest (~> 5.0.0)
rake

BUNDLED WITH
2.5.17
73 changes: 73 additions & 0 deletions test/integration/cli_integration_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

require "test_helper"
require "open3"
require "tmpdir"

module BundleUpdateInteractive
class CLIIntegrationIest < Minitest::Test
def test_updates_lock_file_based_on_selected_gem_while_honoring_gemfile_requirement
out, _gemfile, lockfile = run_bundle_update_interactive(
fixture: "integration",
argv: [],
key_presses: "j \n"
)

assert_includes out, "Color legend:"

assert_includes out, "3 gems can be updated."
assert_includes out, "‣ ⬡ bigdecimal 3.1.7 →"
assert_includes out, " ⬡ minitest 5.0.0 → 5.0.8"
assert_includes out, " ⬡ rake 12.3.3 →"

assert_includes out, "‣ ⬢ minitest 5.0.0 → 5.0.8"

assert_includes out, "Updating the following gems."
assert_includes out, "minitest 5.0.0 → 5.0.8 :default"

assert_includes out, "Bundle updated!"

assert_includes lockfile, <<~LOCK
GEM
remote: https://rubygems.org/
specs:
bigdecimal (3.1.7)
minitest (5.0.8)
LOCK
assert_includes lockfile, <<~LOCK
DEPENDENCIES
bigdecimal
minitest (~> 5.0.0)
LOCK
end

private

def run_bundle_update_interactive(fixture:, argv:, key_presses: "\n")
command = [
{ "GEM_HOME" => ENV.fetch("GEM_HOME", nil) },
Gem.ruby,
"-I",
File.expand_path("../../lib", __dir__),
File.expand_path("../../exe/bundler-update-interactive", __dir__),
*argv
]
within_fixture_copy(fixture) do
Bundler.with_unbundled_env do
out, err, status = Open3.capture3(*command, stdin_data: key_presses)
raise "Command failed: #{[out, err].join}" unless status.success?

[out, File.read("Gemfile"), File.read("Gemfile.lock")]
end
end
end

def within_fixture_copy(fixture, &block)
fixture_path = File.join(File.expand_path("../fixtures", __dir__), fixture)
Dir.mktmpdir do |tmp|
FileUtils.cp_r(fixture_path, tmp)
Dir.chdir(File.join(tmp, File.basename(fixture_path)), &block)
end
end
end
end
8 changes: 8 additions & 0 deletions test/support/mocha.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

require "mocha/minitest"

Mocha.configure do |config|
config.stubbing_method_on_nil = :prevent
config.stubbing_non_existent_method = :prevent
end
1 change: 0 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "bundle_update_interactive"
require "minitest/autorun"
require "mocha/minitest"

BundleUpdateInteractive.pastel = Pastel.new(enabled: true)

Expand Down

0 comments on commit 76a584f

Please sign in to comment.