{"id":15046906,"url":"https://github.com/odlp/jet_black","last_synced_at":"2025-07-18T16:36:04.292Z","repository":{"id":31982950,"uuid":"131184414","full_name":"odlp/jet_black","owner":"odlp","description":"Black-box testing utility for command line tools and gems","archived":false,"fork":false,"pushed_at":"2022-03-06T12:28:16.000Z","size":99,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-01T16:20:59.160Z","etag":null,"topics":["acceptance-testing","blackbox-testing","rspec"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/odlp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-26T16:43:40.000Z","updated_at":"2025-06-10T15:19:27.000Z","dependencies_parsed_at":"2022-08-07T17:01:16.233Z","dependency_job_id":null,"html_url":"https://github.com/odlp/jet_black","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/odlp/jet_black","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odlp%2Fjet_black","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odlp%2Fjet_black/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odlp%2Fjet_black/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odlp%2Fjet_black/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/odlp","download_url":"https://codeload.github.com/odlp/jet_black/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/odlp%2Fjet_black/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265793683,"owners_count":23829180,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["acceptance-testing","blackbox-testing","rspec"],"created_at":"2024-09-24T20:53:43.901Z","updated_at":"2025-07-18T16:36:04.267Z","avatar_url":"https://github.com/odlp.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# JetBlack\n\n[![Gem Version](https://badge.fury.io/rb/jet_black.svg)](https://rubygems.org/gems/jet_black) [![CircleCI](https://circleci.com/gh/odlp/jet_black.svg?style=shield)](https://circleci.com/gh/odlp/jet_black) [![Coverage Status](https://coveralls.io/repos/github/odlp/jet_black/badge.svg?branch=master)](https://coveralls.io/github/odlp/jet_black?branch=master)\n\nA black-box testing utility for command line tools and gems. Written in Ruby,\nwith [RSpec] in mind. Features:\n\n[RSpec]: http://rspec.info/\n\n- Each session takes place within a unique temporary directory, outside the project\n- Synchronously [run commands](#running-commands) then write assertions on:\n  - The `stdout` / `stderr` content\n  - The exit status of the process\n- Exercise [interactive command line interfaces](#running-interactive-commands)\n- Manipulate files in the temporary directory:\n  - [Create files](#file-manipulation)\n  - [Create executable files](#file-manipulation)\n  - [Append content to files](#file-manipulation)\n  - [Copy fixture files](#copying-fixture-files) from your project\n- Modify the environment without changing the parent test process:\n  - [Override environment variables](#environment-variable-overrides)\n  - [Escape the current Bundler context](#clean-bundler-environment)\n  - [Adjust `$PATH`](#path-prefix) to include your executable / Subject Under Test\n- [RSpec matchers](#rspec-matchers) (optional)\n\nThe temporary directory is discarded after each spec. This means you can write \u0026\nmodify files and run commands (like `git init`) without worrying about tidying\nup after or impacting your actual project.\n\n## Setup\n\n```ruby\ngroup :test do\n  gem \"jet_black\"\nend\n```\n\n### RSpec setup\n\nIf you're using RSpec, you can load matchers with the following require\n(optional):\n\n```ruby\n# spec/spec_helper.rb\n\nrequire \"jet_black/rspec\"\n```\n\nAny specs you write in the `spec/black_box` folder will then have an inferred\n`:black_box` meta type, and the matchers will be available in those examples.\n\n#### Manual RSpec setup\n\nAlternatively you can manually include the matchers:\n\n```ruby\n# spec/cli/example_spec.rb\n\nrequire \"jet_black\"\nrequire \"jet_black/rspec/matchers\"\n\nRSpec.describe \"my command line tool\" do\n  include JetBlack::RSpec::Matchers\nend\n```\n\n## Usage\n\n### Running commands\n\n```ruby\nrequire \"jet_black\"\n\nsession = JetBlack::Session.new\nresult = session.run(\"echo foo\")\n\nresult.stdout # =\u003e \"foo\\n\"\nresult.stderr # =\u003e \"\"\nresult.exit_status # =\u003e 0\n```\n\nProviding `stdin` data:\n\n```ruby\nsession = JetBlack::Session.new\nsession.run(\"./hello-world\", stdin: \"Alice\")\n```\n\n### Running interactive commands\n\n```ruby\nsession = JetBlack::Session.new\n\nresult = session.run_interactive(\"./hello-world\") do |terminal|\n  terminal.expect(\"What's your name?\", reply: \"Alice\")\n  terminal.expect(\"What's your location?\", reply: \"Wonderland\")\nend\n\nexpect(result.exit_status).to eq 0\nexpect(result.stdout).to eq \u003c\u003c~TXT\n  What's your name?\n  Alice\n  What's your location?\n  Wonderland\n  Hello Alice in Wonderland\nTXT\n```\n\nIf you don't want to wait for a process to finish, you can end the interactive\nsession early:\n\n```ruby\nsession = JetBlack::Session.new\n\nresult = session.run_interactive(\"./long-cli-flow\") do |terminal|\n  terminal.expect(\"Question 1\", reply: \"Y\")\n  terminal.end_session(signal: \"INT\")\nend\n```\n\n### File manipulation\n\n```ruby\nsession = JetBlack::Session.new\n\nsession.create_file \"file.txt\", \u003c\u003c~TXT\n  The quick brown fox\n  jumps over the lazy dog\nTXT\n\nsession.create_executable \"hello-world.sh\", \u003c\u003c~SH\n  #!/bin/sh\n  echo \"Hello world\"\nSH\n\nsession.append_to_file \"file.txt\", \u003c\u003c~TXT\n  shiny\n  new\n  lines\nTXT\n\n# Subdirectories are created for you:\nsession.create_file \"deeper/underground/jamiroquai.txt\", \u003c\u003c~TXT\n  I'm going deeper underground, hey ha\n  There's too much panic in this town\nTXT\n```\n\n### Copying fixture files\n\nIt's ideal to create pertinent files inline within a spec, to provide context\nfor the reader, but sometimes it's better to copy across a large or\nnon-human-readable file.\n\n1.    Create a fixture directory in your project, such as `spec/fixtures/black_box`.\n\n2.    Configure the fixture path in `spec/support/jet_black.rb`:\n\n      ```ruby\n      require \"jet_black\"\n\n      JetBlack.configure do |config|\n        config.fixture_directory = File.expand_path(\"../fixtures/black_box\", __dir__)\n      end\n      ```\n\n3.    Copy fixtures across into a session's temporary directory:\n\n      ```ruby\n      session = JetBlack::Session.new\n      session.copy_fixture(\"src-config.json\", \"config.json\")\n\n      # Destination subdirectories are created for you:\n      session.copy_fixture(\"src-config.json\", \"config/config.json\")\n      ```\n\n### Environment variable overrides\n\n```ruby\nsession = JetBlack::Session.new\nresult = session.run(\"printf $FOO\", env: { FOO: \"bar\" })\n\nresult.stdout # =\u003e \"bar\"\n```\n\nProvide a `nil` value to unset an environment variable.\n\n### Clean Bundler environment\n\nIf your project's test suite is invoked with Bundler (e.g. `bundle exec rspec`)\nbut you want to run commands like `bundle install` and `bundle exec` with a\ndifferent Gemfile in a given spec, you can configure the session or individual\ncommands to run with a clean Bundler environment.\n\nPer command:\n\n```ruby\nsession = JetBlack::Session.new\nsession.run(\"bundle install\", options: { clean_bundler_env: true })\n```\n\nPer session:\n\n```ruby\nsession = JetBlack::Session.new(options: { clean_bundler_env: true })\nsession.run(\"bundle install\")\nsession.run(\"bundle exec rake\")\n```\n\n### `$PATH` prefix\n\nGiven the root of your project contains a `bin` directory containing\n`my_awesome_bin`.\n\nConfigure the `path_prefix` to the directory containing your executable(s):\n\n```ruby\n# spec/support/jet_black.rb\n\nrequire \"jet_black\"\n\nJetBlack.configure do |config|\n  config.path_prefix = File.expand_path(\"../../bin\", __dir__)\nend\n```\n\nThen the `$PATH` of each session will include the configured directory, and your\nexecutable should be invokable:\n\n```ruby\nJetBlack::Session.new.run(\"my_awesome_bin\")\n```\n\n### RSpec matchers\n\nGiven the [RSpec setup](#rspec-setup) is configured, you'll have access to the\nfollowing matchers:\n\n- `have_stdout` which accepts a string or regular expression\n- `have_stderr` which accepts a string or regular expression\n- `have_no_stdout` which asserts the `stdout` is empty\n- `have_no_stderr` which asserts the `stderr` is empty\n\nAnd the following predicate matchers:\n\n- `be_a_success` / `be_success` asserts the exit status was zero\n- `be_a_failure` / `be_failure` asserts the exit status was not zero\n\n#### Example assertions\n\n```ruby\n# spec/black_box/cli_spec.rb\n\nRSpec.describe \"my command line tool\" do\n  let(:session) { JetBlack::Session.new }\n\n  it \"does the work\" do\n    expect(session.run(\"my_tool --good\")).\n      to be_a_success.and have_stdout(/It worked/)\n  end\n\n  it \"explodes with incorrect arguments\" do\n    expect(session.run(\"my_tool --bad\")).\n      to be_a_failure.and have_stderr(\"Oh no!\")\n  end\nend\n```\n\nHowever these assertions can be made with built-in matchers too:\n\n```ruby\nRSpec.describe \"my command line tool\" do\n  let(:session) { JetBlack::Session.new }\n\n  it \"does the work\" do\n    result = session.run(\"my_tool --good\")\n\n    expect(result.stdout).to match(/It worked/)\n    expect(result.exit_status).to eq 0\n  end\n\n  it \"explodes with incorrect arguments\" do\n    result = session.run(\"my_tool --bad\")\n\n    expect(result.stderr).to match(\"Oh no!\")\n    expect(result.exit_status).to eq 1\n  end\nend\n```\n\n## More examples\n\n- JetBlack's own [higher-level tests](https://github.com/odlp/jet_black/tree/master/spec/features)\n- A more complex scenario testing a [gem in a fresh Rails app](https://github.com/thoughtbot/capybara_discoball/blob/master/spec/black_box/rails_app_spec.rb#L8-L39). Shows how to:\n  - Include the [gem-under-test via the Rails app's Gemfile](https://github.com/thoughtbot/capybara_discoball/blob/4e89bfe5531eea1bf6dac42c46c26d0c687d6ddf/spec/black_box/rails_app_spec.rb#L99-L104)\n  - Use a [clean Bundler environment](https://github.com/thoughtbot/capybara_discoball/blob/4e89bfe5531eea1bf6dac42c46c26d0c687d6ddf/spec/black_box/rails_app_spec.rb#L5) to use the Gemfile of the new Rails app (instead of the Bundler context of the gem's test suite)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fodlp%2Fjet_black","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fodlp%2Fjet_black","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fodlp%2Fjet_black/lists"}