constable-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +515 -0
- data/exe/constable +7 -0
- data/lib/constable/case.rb +336 -0
- data/lib/constable/cli.rb +475 -0
- data/lib/constable/cold_case/minitest.rb +342 -0
- data/lib/constable/cold_case/rspec.rb +334 -0
- data/lib/constable/cold_case.rb +280 -0
- data/lib/constable/config.rb +125 -0
- data/lib/constable/coverage.rb +951 -0
- data/lib/constable/diff.rb +212 -0
- data/lib/constable/dsl.rb +833 -0
- data/lib/constable/identity.rb +121 -0
- data/lib/constable/importer/modernizer.rb +860 -0
- data/lib/constable/importer/reopener.rb +468 -0
- data/lib/constable/importer.rb +51 -0
- data/lib/constable/investigation.rb +67 -0
- data/lib/constable/isolation.rb +171 -0
- data/lib/constable/jail.rb +399 -0
- data/lib/constable/log_router.rb +197 -0
- data/lib/constable/matchers.rb +834 -0
- data/lib/constable/order_audit.rb +130 -0
- data/lib/constable/rails_support.rb +213 -0
- data/lib/constable/railtie.rb +36 -0
- data/lib/constable/registry.rb +57 -0
- data/lib/constable/reporter.rb +625 -0
- data/lib/constable/result.rb +149 -0
- data/lib/constable/runner.rb +697 -0
- data/lib/constable/selection.rb +205 -0
- data/lib/constable/storage/adapter.rb +91 -0
- data/lib/constable/storage/mysql_adapter.rb +125 -0
- data/lib/constable/storage/postgres_adapter.rb +125 -0
- data/lib/constable/storage/sqlite_adapter.rb +84 -0
- data/lib/constable/storage.rb +847 -0
- data/lib/constable/version.rb +5 -0
- data/lib/constable/warrants.rb +290 -0
- data/lib/constable-rails.rb +16 -0
- data/lib/constable.rb +151 -0
- data/lib/generators/constable/base.rb +99 -0
- data/lib/generators/constable/channel/channel_generator.rb +20 -0
- data/lib/generators/constable/channel/templates/channel_case.rb.tt +29 -0
- data/lib/generators/constable/controller/controller_generator.rb +25 -0
- data/lib/generators/constable/controller/templates/controller_case.rb.tt +32 -0
- data/lib/generators/constable/generator/generator_generator.rb +31 -0
- data/lib/generators/constable/generator/templates/generator_case.rb.tt +28 -0
- data/lib/generators/constable/helper/helper_generator.rb +23 -0
- data/lib/generators/constable/helper/templates/helper_case.rb.tt +19 -0
- data/lib/generators/constable/import_generator.rb +137 -0
- data/lib/generators/constable/install_generator.rb +188 -0
- data/lib/generators/constable/integration/integration_generator.rb +27 -0
- data/lib/generators/constable/integration/templates/request_case.rb.tt +22 -0
- data/lib/generators/constable/job/job_generator.rb +20 -0
- data/lib/generators/constable/job/templates/job_case.rb.tt +33 -0
- data/lib/generators/constable/mailbox/mailbox_generator.rb +20 -0
- data/lib/generators/constable/mailbox/templates/mailbox_case.rb.tt +26 -0
- data/lib/generators/constable/mailer/mailer_generator.rb +32 -0
- data/lib/generators/constable/mailer/templates/mailer_case.rb.tt +34 -0
- data/lib/generators/constable/mailer/templates/preview.rb.tt +14 -0
- data/lib/generators/constable/model/model_generator.rb +31 -0
- data/lib/generators/constable/model/templates/model_case.rb.tt +37 -0
- data/lib/generators/constable/resource/resource_generator.rb +27 -0
- data/lib/generators/constable/scaffold/scaffold_generator.rb +42 -0
- data/lib/generators/constable/scaffold/templates/api_controller_case.rb.tt +54 -0
- data/lib/generators/constable/scaffold/templates/controller_case.rb.tt +70 -0
- data/lib/generators/constable/scaffold/templates/system_case.rb.tt +53 -0
- data/lib/generators/constable/system/system_generator.rb +20 -0
- data/lib/generators/constable/system/templates/system_case.rb.tt +18 -0
- data/lib/generators/constable/templates/authenticatable.rb.tt +31 -0
- data/lib/generators/constable/templates/case_helper.rb.tt +179 -0
- data/lib/generators/constable/templates/config.yml.tt +67 -0
- data/lib/generators/constable/templates/example_case.rb.tt +56 -0
- data/lib/generators/constable/templates/matchers.rb.tt +36 -0
- data/lib/generators/constable/templates/rubocop.yml.tt +12 -0
- metadata +209 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate model Post title:string` (and by anything else that ends
|
|
8
|
+
# up at active_record:model -- `rails generate resource`, `rails generate scaffold`).
|
|
9
|
+
#
|
|
10
|
+
# test/cases/models/post_case.rb class PostCase < UnitCase
|
|
11
|
+
class ModelGenerator < Base
|
|
12
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
13
|
+
|
|
14
|
+
# Accepted and ignored, so `rails generate model --fixture` still runs for an app
|
|
15
|
+
# that passes it out of habit. Constable has no fixtures: a witness builds exactly
|
|
16
|
+
# what one investigation needs and throws it away with it, which is the same reason
|
|
17
|
+
# there is no before(:all).
|
|
18
|
+
class_option :fixture, type: :boolean
|
|
19
|
+
|
|
20
|
+
check_class_collision suffix: "Case"
|
|
21
|
+
|
|
22
|
+
def create_case_file
|
|
23
|
+
template "model_case.rb.tt", case_path("models", class_path, "#{file_name}_case.rb")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A factory gem registered as the fixture replacement still gets its turn -- factories
|
|
27
|
+
# are a witness's business, not a fixture's, and Constable has no quarrel with them.
|
|
28
|
+
hook_for :fixture_replacement
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
# Generated by `rails generate model <%= name %>`.
|
|
7
|
+
#
|
|
8
|
+
# :unit is the fastest tier Constable has, and the one thing it deliberately does not do
|
|
9
|
+
# is wrap each investigation in a transaction that gets rolled back afterwards. So build
|
|
10
|
+
# records here, don't save them -- anything that has to persist belongs in an
|
|
11
|
+
# :integration-tier case, where the rollback exists to clean up after it.
|
|
12
|
+
class <%= class_name %>Case < UnitCase
|
|
13
|
+
# Memoized per investigation and thrown away with it. Name it five times in one test
|
|
14
|
+
# and pay for it once; the next test builds its own.
|
|
15
|
+
witness(:<%= singular_name %>) { <%= new_record_expression %> }
|
|
16
|
+
|
|
17
|
+
<% if case_attributes.any? -%>
|
|
18
|
+
# The honest first assertion, and the one that earns its keep: the class loads, the
|
|
19
|
+
# table is there, and every column this generator just declared reads back. That is
|
|
20
|
+
# what catches a migration written but never run.
|
|
21
|
+
investigate "reads back the attributes it was generated with" do
|
|
22
|
+
<% case_attributes.each do |attribute| -%>
|
|
23
|
+
attest(<%= singular_name %>.<%= attribute.column_name %>).to eq(<%= attribute_value(attribute) %>)
|
|
24
|
+
<% end -%>
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
<% end -%>
|
|
28
|
+
# Then the first real one -- a validation, an association, a method this model owns:
|
|
29
|
+
#
|
|
30
|
+
# investigate "requires a <%= case_attributes.first&.column_name || "name" %>" do
|
|
31
|
+
# <%= singular_name %>.<%= case_attributes.first&.column_name || "name" %> = nil
|
|
32
|
+
#
|
|
33
|
+
# refute <%= singular_name %>.valid?
|
|
34
|
+
# attest(<%= singular_name %>.errors[:<%= case_attributes.first&.column_name || "name" %>]).to include("can't be blank")
|
|
35
|
+
# end
|
|
36
|
+
end
|
|
37
|
+
<% end -%>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/model/model_generator"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate resource Post title:string` is a model plus a routed controller, and
|
|
8
|
+
# Rails builds it that way: Rails::Generators::ResourceGenerator inherits from the
|
|
9
|
+
# model generator and hooks the controller separately. So the test-framework side of a
|
|
10
|
+
# resource is the model case -- the controller half arrives through constable:controller,
|
|
11
|
+
# invoked by the resource_controller hook.
|
|
12
|
+
#
|
|
13
|
+
# This subclass exists so that lookup resolves rather than printing
|
|
14
|
+
# "constable:resource [not found]" for anything that asks for the resource generator by
|
|
15
|
+
# name, and so it delegates exactly the way Rails' own does: same superclass, same
|
|
16
|
+
# output, no second copy of the template.
|
|
17
|
+
class ResourceGenerator < ModelGenerator
|
|
18
|
+
# generator_name is "resource", which would send the inherited source_root looking in
|
|
19
|
+
# a templates directory this generator does not have and does not need.
|
|
20
|
+
def self.source_root(path = nil)
|
|
21
|
+
return super if path
|
|
22
|
+
|
|
23
|
+
ModelGenerator.source_root
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
require "rails/generators/resource_helpers"
|
|
5
|
+
|
|
6
|
+
module Constable
|
|
7
|
+
module Generators
|
|
8
|
+
# Invoked by `rails generate scaffold Post title:string`, via rails:scaffold_controller,
|
|
9
|
+
# which hooks :test_framework as :scaffold.
|
|
10
|
+
#
|
|
11
|
+
# test/cases/controllers/posts_controller_case.rb class PostsControllerCase < IntegrationCase
|
|
12
|
+
# test/cases/system/posts_case.rb class PostsCase < SystemCase
|
|
13
|
+
#
|
|
14
|
+
# The system case is written only when Rails asks for one (`--system-tests=true`),
|
|
15
|
+
# matching test_unit exactly: a browser test that nobody asked for is the slowest
|
|
16
|
+
# possible way to find that out.
|
|
17
|
+
class ScaffoldGenerator < Base
|
|
18
|
+
include ::Rails::Generators::ResourceHelpers
|
|
19
|
+
|
|
20
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
21
|
+
|
|
22
|
+
class_option :api, type: :boolean,
|
|
23
|
+
desc: "Generate cases for an API-only controller"
|
|
24
|
+
class_option :system_tests, type: :string,
|
|
25
|
+
desc: "Generate a system case (set to 'true' to enable)"
|
|
26
|
+
|
|
27
|
+
check_class_collision suffix: "ControllerCase"
|
|
28
|
+
|
|
29
|
+
def create_controller_case
|
|
30
|
+
template options.api? ? "api_controller_case.rb.tt" : "controller_case.rb.tt",
|
|
31
|
+
case_path("controllers", controller_class_path, "#{controller_file_name}_controller_case.rb")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_system_case
|
|
35
|
+
return if options.api?
|
|
36
|
+
return unless options[:system_tests] == "true"
|
|
37
|
+
|
|
38
|
+
template "system_case.rb.tt", case_path("system", class_path, "#{plural_file_name}_case.rb")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
# Generated by `rails generate scaffold <%= name %> --api`. One investigation per action,
|
|
7
|
+
# every request and response in JSON -- no form pages, so no new/edit.
|
|
8
|
+
class <%= controller_class_name %>ControllerCase < IntegrationCase
|
|
9
|
+
<% if mountable_engine? -%>
|
|
10
|
+
include Engine.routes.url_helpers
|
|
11
|
+
|
|
12
|
+
<% end -%>
|
|
13
|
+
witness(:valid_attributes) { <%= attributes_literal %> }
|
|
14
|
+
witness(:existing_<%= singular_table_name %>) { <%= class_name %>.create!(valid_attributes) }
|
|
15
|
+
|
|
16
|
+
investigate "index responds successfully" do
|
|
17
|
+
get <%= index_helper(type: :url) %>, as: :json
|
|
18
|
+
|
|
19
|
+
attest(response).to have_http_status(:ok)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
investigate "create saves a <%= human_name.downcase %>" do
|
|
23
|
+
assert_difference("<%= class_name %>.count", 1) do
|
|
24
|
+
post <%= index_helper(type: :url) %>, params: { <%= singular_table_name %>: valid_attributes }, as: :json
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attest(response).to have_http_status(:created)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
investigate "show responds successfully" do
|
|
31
|
+
get <%= show_helper("existing_#{singular_table_name}") %>, as: :json
|
|
32
|
+
|
|
33
|
+
attest(response).to have_http_status(:ok)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
investigate "update responds successfully" do
|
|
37
|
+
patch <%= show_helper("existing_#{singular_table_name}") %>, params: { <%= singular_table_name %>: valid_attributes }, as: :json
|
|
38
|
+
|
|
39
|
+
attest(response).to have_http_status(:ok)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
investigate "destroy removes the <%= human_name.downcase %>" do
|
|
43
|
+
# Named before the block on purpose: a witness is built the first time it is used, so
|
|
44
|
+
# creating it inside assert_difference would net out to zero.
|
|
45
|
+
record = existing_<%= singular_table_name %>
|
|
46
|
+
|
|
47
|
+
assert_difference("<%= class_name %>.count", -1) do
|
|
48
|
+
delete <%= show_helper("record") %>, as: :json
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attest(response).to have_http_status(:no_content)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
<% end -%>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
# Generated by `rails generate scaffold <%= name %>`. One investigation per action the
|
|
7
|
+
# scaffold just wrote, which is a floor rather than a suite: the case worth writing is the
|
|
8
|
+
# one about behavior this controller has that no scaffold does.
|
|
9
|
+
class <%= controller_class_name %>ControllerCase < IntegrationCase
|
|
10
|
+
<% if mountable_engine? -%>
|
|
11
|
+
include Engine.routes.url_helpers
|
|
12
|
+
|
|
13
|
+
<% end -%>
|
|
14
|
+
# :integration wraps every investigation in a transaction that is rolled back
|
|
15
|
+
# afterwards, so a witness may persist a record. It is still built fresh for each one --
|
|
16
|
+
# nothing here survives into the next investigation.
|
|
17
|
+
witness(:valid_attributes) { <%= attributes_literal %> }
|
|
18
|
+
witness(:existing_<%= singular_table_name %>) { <%= class_name %>.create!(valid_attributes) }
|
|
19
|
+
|
|
20
|
+
investigate "index responds successfully" do
|
|
21
|
+
get <%= index_helper(type: :url) %>
|
|
22
|
+
|
|
23
|
+
attest(response).to have_http_status(:ok)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
investigate "new renders the form" do
|
|
27
|
+
get <%= new_helper %>
|
|
28
|
+
|
|
29
|
+
attest(response).to have_http_status(:ok)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
investigate "create saves a <%= human_name.downcase %> and redirects to it" do
|
|
33
|
+
assert_difference("<%= class_name %>.count", 1) do
|
|
34
|
+
post <%= index_helper(type: :url) %>, params: { <%= singular_table_name %>: valid_attributes }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attest(response).to redirect_to(<%= show_helper("#{class_name}.last") %>)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
investigate "show responds successfully" do
|
|
41
|
+
get <%= show_helper("existing_#{singular_table_name}") %>
|
|
42
|
+
|
|
43
|
+
attest(response).to have_http_status(:ok)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
investigate "edit renders the form" do
|
|
47
|
+
get <%= edit_helper("existing_#{singular_table_name}") %>
|
|
48
|
+
|
|
49
|
+
attest(response).to have_http_status(:ok)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
investigate "update redirects to the <%= human_name.downcase %>" do
|
|
53
|
+
patch <%= show_helper("existing_#{singular_table_name}") %>, params: { <%= singular_table_name %>: valid_attributes }
|
|
54
|
+
|
|
55
|
+
attest(response).to redirect_to(<%= show_helper("existing_#{singular_table_name}") %>)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
investigate "destroy removes the <%= human_name.downcase %> and redirects to the index" do
|
|
59
|
+
# Named before the block on purpose: a witness is built the first time it is used, so
|
|
60
|
+
# creating it inside assert_difference would net out to zero.
|
|
61
|
+
record = existing_<%= singular_table_name %>
|
|
62
|
+
|
|
63
|
+
assert_difference("<%= class_name %>.count", -1) do
|
|
64
|
+
delete <%= show_helper("record") %>
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attest(response).to redirect_to(<%= index_helper(type: :url) %>)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
<% end -%>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
# Generated by `rails generate scaffold <%= name %> --system-tests=true`.
|
|
7
|
+
#
|
|
8
|
+
# System cases drive a real browser, so they are the slowest thing Constable runs and the
|
|
9
|
+
# easiest place in a suite to write a flaky test. Two rules cover most of it: never sleep
|
|
10
|
+
# (Capybara's finders already wait, and a bare sleep is a lint error anyway), and assert
|
|
11
|
+
# on what the page shows rather than on how long it took to show it.
|
|
12
|
+
class <%= class_name.pluralize %>Case < SystemCase
|
|
13
|
+
witness(:valid_attributes) { <%= attributes_literal %> }
|
|
14
|
+
witness(:existing_<%= singular_table_name %>) { <%= class_name %>.create!(valid_attributes) }
|
|
15
|
+
|
|
16
|
+
investigate "visiting the index" do
|
|
17
|
+
visit <%= index_helper(type: :url) %>
|
|
18
|
+
|
|
19
|
+
assert page.has_selector?("h1", text: "<%= human_name.pluralize %>")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
investigate "creating a <%= human_name.downcase %>" do
|
|
23
|
+
visit <%= index_helper(type: :url) %>
|
|
24
|
+
click_on "New <%= human_name.downcase %>"
|
|
25
|
+
|
|
26
|
+
<% case_attributes.each do |attribute| -%>
|
|
27
|
+
<% if attribute.type == :boolean -%>
|
|
28
|
+
check "<%= attribute.human_name %>"
|
|
29
|
+
<% else -%>
|
|
30
|
+
fill_in "<%= attribute.human_name %>", with: <%= attribute_value(attribute) %>
|
|
31
|
+
<% end -%>
|
|
32
|
+
<% end -%>
|
|
33
|
+
click_on "Create <%= human_name %>"
|
|
34
|
+
|
|
35
|
+
attest(page.text).to include("<%= human_name %> was successfully created")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
investigate "updating a <%= human_name.downcase %>" do
|
|
39
|
+
visit <%= show_helper("existing_#{singular_table_name}") %>
|
|
40
|
+
click_on "Edit this <%= human_name.downcase %>", match: :first
|
|
41
|
+
click_on "Update <%= human_name %>"
|
|
42
|
+
|
|
43
|
+
attest(page.text).to include("<%= human_name %> was successfully updated")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
investigate "destroying a <%= human_name.downcase %>" do
|
|
47
|
+
visit <%= show_helper("existing_#{singular_table_name}") %>
|
|
48
|
+
click_on "Destroy this <%= human_name.downcase %>", match: :first
|
|
49
|
+
|
|
50
|
+
attest(page.text).to include("<%= human_name %> was successfully destroyed")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
<% end -%>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "generators/constable/base"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
module Generators
|
|
7
|
+
# Invoked by `rails generate system_test Posts`, which hooks :system_tests as :system.
|
|
8
|
+
#
|
|
9
|
+
# test/cases/system/posts_case.rb class PostsCase < SystemCase
|
|
10
|
+
class SystemGenerator < Base
|
|
11
|
+
check_class_collision suffix: "Case"
|
|
12
|
+
|
|
13
|
+
strips_suffix(/_test\z/i)
|
|
14
|
+
|
|
15
|
+
def create_case_file
|
|
16
|
+
template "system_case.rb.tt", case_path("system", class_path, "#{plural_file_name}_case.rb")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "case_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= class_name.pluralize %>Case < SystemCase
|
|
7
|
+
# System cases drive a real browser, so they are the slowest thing Constable runs and
|
|
8
|
+
# the easiest place in a suite to write a flaky test. Two rules cover most of it: never
|
|
9
|
+
# sleep (Capybara's finders already wait, and a bare sleep is a lint error anyway), and
|
|
10
|
+
# assert on what the page shows rather than on how long it took to show it.
|
|
11
|
+
#
|
|
12
|
+
# investigate "visiting the index" do
|
|
13
|
+
# visit <%= plural_table_name %>_url
|
|
14
|
+
#
|
|
15
|
+
# assert page.has_selector?("h1", text: "<%= class_name.pluralize %>")
|
|
16
|
+
# end
|
|
17
|
+
end
|
|
18
|
+
<% end -%>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared behavior is just a module.
|
|
4
|
+
#
|
|
5
|
+
# There is deliberately no shared-examples DSL in Constable. `include` already
|
|
6
|
+
# does that job: it composes, it respects ancestry, it shows up in `.ancestors`,
|
|
7
|
+
# your editor can jump to the definition, and there is no second set of scoping
|
|
8
|
+
# rules to learn on top of Ruby's own.
|
|
9
|
+
#
|
|
10
|
+
# Any case can pick this up directly:
|
|
11
|
+
#
|
|
12
|
+
# class SessionsCase < IntegrationCase
|
|
13
|
+
# include Authenticatable
|
|
14
|
+
#
|
|
15
|
+
# investigate "signs a user in" do
|
|
16
|
+
# sign_in(witnessed_user)
|
|
17
|
+
# attest(response).to redirect_to(root_path)
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Or a whole tier can, by adding `include Authenticatable` to IntegrationCase in
|
|
22
|
+
# test/case_helper.rb.
|
|
23
|
+
#
|
|
24
|
+
# One rule applies here exactly as it does inside a case: a support module must
|
|
25
|
+
# not hold mutable state across tests. Methods and constants, not @@class_vars.
|
|
26
|
+
|
|
27
|
+
module Authenticatable
|
|
28
|
+
def sign_in(user)
|
|
29
|
+
post session_path, params: { email: user.email, password: "password" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# test/case_helper.rb
|
|
5
|
+
#
|
|
6
|
+
# Constable's equivalent of RSpec's spec_helper.rb + rails_helper.rb, or of
|
|
7
|
+
# Minitest's test_helper.rb. `constable test` loads this file once, before any
|
|
8
|
+
# case file -- individual cases never require it themselves.
|
|
9
|
+
#
|
|
10
|
+
# Three things live here:
|
|
11
|
+
#
|
|
12
|
+
# 1. Booting the Rails test environment.
|
|
13
|
+
# 2. The per-tier base classes your cases inherit from.
|
|
14
|
+
# 3. Constable.configure -- configuration that is *code*.
|
|
15
|
+
#
|
|
16
|
+
# Configuration that is merely a *setting* (a number, a flag, a glob) lives in
|
|
17
|
+
# .constable/config.yml instead, so it can be overridden by a CLI flag for one
|
|
18
|
+
# run without editing Ruby.
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
22
|
+
|
|
23
|
+
require_relative "../config/environment"
|
|
24
|
+
require "constable"
|
|
25
|
+
|
|
26
|
+
# A test suite that can reach production data is a suite that will eventually
|
|
27
|
+
# destroy some. This costs one string comparison per run.
|
|
28
|
+
abort("The Rails environment is running in production mode!") if Rails.env.production?
|
|
29
|
+
|
|
30
|
+
# Loads db/schema.rb into the test database when it has drifted behind your
|
|
31
|
+
# migrations -- the same thing `rails test` does before it runs anything. Without
|
|
32
|
+
# it the first case to touch the database fails with "no such table", which tells
|
|
33
|
+
# you nothing about the test you actually wrote.
|
|
34
|
+
if defined?(ActiveRecord::Migration)
|
|
35
|
+
begin
|
|
36
|
+
ActiveRecord::Migration.maintain_test_schema!
|
|
37
|
+
rescue ActiveRecord::PendingMigrationError => e
|
|
38
|
+
abort("#{e.message}\n\nRun: bin/rails db:migrate RAILS_ENV=test")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# -----------------------------------------------------------------------------
|
|
43
|
+
# Tiers are base classes, not magic.
|
|
44
|
+
#
|
|
45
|
+
# A tier says how much of the stack a case needs: :unit skips the database and
|
|
46
|
+
# the request stack entirely and is the fastest thing Constable can run,
|
|
47
|
+
# :integration gets a transactional database plus request helpers, :system
|
|
48
|
+
# additionally drives a browser.
|
|
49
|
+
#
|
|
50
|
+
# Rather than infer that from a file path, declare it once on a base class and
|
|
51
|
+
# let ordinary Ruby inheritance carry it down:
|
|
52
|
+
#
|
|
53
|
+
# class UsersController::CreatesUserCase < IntegrationCase
|
|
54
|
+
#
|
|
55
|
+
# Path-based inference (the `tiers:` section of .constable/config.yml) still
|
|
56
|
+
# works as a fallback for cases that don't inherit from one of these, but
|
|
57
|
+
# inheritance is the recommended pattern -- there is nothing to infer, nothing
|
|
58
|
+
# to surprise you, and no rule to learn that Ruby didn't already teach you.
|
|
59
|
+
#
|
|
60
|
+
# These are plain classes. Anything a whole tier needs belongs here: an
|
|
61
|
+
# ActiveJob test adapter, a factory module, `include Authenticatable` for every
|
|
62
|
+
# integration case.
|
|
63
|
+
#
|
|
64
|
+
# The two Constable::RailsSupport modules below are the bridge to Rails' own
|
|
65
|
+
# testing code. Constable does not reimplement `post users_path` or
|
|
66
|
+
# `click_on "Save"` -- Rails ships both as ordinary modules, and every
|
|
67
|
+
# reimplementation of them would be a slightly wrong one.
|
|
68
|
+
# -----------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
# No database, no request stack, no browser -- just Ruby. The fastest thing
|
|
71
|
+
# Constable can run, and where most of a suite should live.
|
|
72
|
+
class UnitCase < Constable::Case
|
|
73
|
+
tier :unit
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# get/post/patch/delete, `response`, `follow_redirect!`, cookies, and every URL
|
|
77
|
+
# helper your routes define (`users_path`, `article_url(article)`). Each
|
|
78
|
+
# investigation runs inside a transaction that is rolled back afterwards.
|
|
79
|
+
class IntegrationCase < Constable::Case
|
|
80
|
+
include Constable::RailsSupport::Integration
|
|
81
|
+
|
|
82
|
+
tier :integration
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The browser instead of the request stack: Capybara's `visit`/`click_on`/
|
|
86
|
+
# `fill_in`/`page`, its assertions, your URL helpers pointed at the test server,
|
|
87
|
+
# and Rails' own `driven_by`. There is no `response` here -- a system case looks
|
|
88
|
+
# at the rendered page, which is the whole reason to pay for a browser.
|
|
89
|
+
class SystemCase < Constable::Case
|
|
90
|
+
# Capybara is not a Constable dependency. System cases are opt-in, and a suite
|
|
91
|
+
# with no browser tests shouldn't be made to install a browser driver to boot.
|
|
92
|
+
include Constable::RailsSupport::System if defined?(Capybara)
|
|
93
|
+
|
|
94
|
+
tier :system
|
|
95
|
+
|
|
96
|
+
# The default is Selenium driving Chrome, exactly as Rails' own system tests
|
|
97
|
+
# default. Change it here and every system case follows:
|
|
98
|
+
#
|
|
99
|
+
# driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# -----------------------------------------------------------------------------
|
|
103
|
+
# Support files -- shared modules and custom matchers.
|
|
104
|
+
#
|
|
105
|
+
# Same role RSpec's spec/support/**/*.rb plays. Shared behavior *across* files
|
|
106
|
+
# is a plain module you `include`; there is deliberately no shared-examples DSL
|
|
107
|
+
# here, because Ruby's own composition tools already do that job with fewer
|
|
108
|
+
# rules to learn and more flexibility once the reuse stops being simple.
|
|
109
|
+
#
|
|
110
|
+
# Sorted on purpose: Dir[] returns filesystem order, which differs between your
|
|
111
|
+
# laptop and CI. A suite that loads its own support files in an unpredictable
|
|
112
|
+
# order has already lost the argument about determinism.
|
|
113
|
+
# -----------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
Dir[Rails.root.join("test/support/**/*.rb")].sort.each { |f| require f }
|
|
116
|
+
|
|
117
|
+
# -----------------------------------------------------------------------------
|
|
118
|
+
# Code-level configuration.
|
|
119
|
+
# -----------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
Constable.configure do |c|
|
|
122
|
+
# Workers default to `auto` -- processor count minus a little headroom, so the
|
|
123
|
+
# machine stays usable while the suite runs. Pin it only when a CI container
|
|
124
|
+
# lies about its core count.
|
|
125
|
+
# c.parallel_workers = 4
|
|
126
|
+
|
|
127
|
+
# One-time global setup, run once per process before the whole suite. This is
|
|
128
|
+
# for configuring the world -- drivers, adapters, formats -- and deliberately
|
|
129
|
+
# not for creating records that tests then share. See the note at the bottom
|
|
130
|
+
# of this file about why that distinction is the whole ballgame.
|
|
131
|
+
#
|
|
132
|
+
# c.before_suite do
|
|
133
|
+
# Capybara.default_driver = :rack_test
|
|
134
|
+
# Capybara.javascript_driver = :headless_chrome
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
# c.after_suite do
|
|
138
|
+
# SomeExternalHarness.shut_down
|
|
139
|
+
# end
|
|
140
|
+
|
|
141
|
+
# Custom matchers can be defined here too, though test/support/matchers.rb is
|
|
142
|
+
# the better home once there is more than one of them:
|
|
143
|
+
#
|
|
144
|
+
# Constable::Matchers.define(:be_created) { |response| response.status == 201 }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# Two things are missing from this file on purpose.
|
|
149
|
+
#
|
|
150
|
+
# 1. There is no before(:all), no setup_once, no one-time fixture hook.
|
|
151
|
+
#
|
|
152
|
+
# A before(:all) block builds state once and hands the same objects to many
|
|
153
|
+
# tests. That is faster right up until one test mutates one of them -- and
|
|
154
|
+
# then you have a bug that appears only in a particular order, on a
|
|
155
|
+
# particular machine, in CI, roughly once a week. The ordering that produced
|
|
156
|
+
# it isn't written down anywhere, so it isn't reproducible, so it doesn't get
|
|
157
|
+
# fixed: it gets a retry, and the retry gets copied to the next test.
|
|
158
|
+
#
|
|
159
|
+
# Constable doesn't have the hook, so it can't have the bug. `briefing` runs
|
|
160
|
+
# before *every* investigation, against a fresh instance, inside a
|
|
161
|
+
# transaction that is rolled back afterwards. When that is genuinely too
|
|
162
|
+
# slow, the fix is a cheaper fixture -- build_stubbed instead of create, one
|
|
163
|
+
# record instead of five -- not state shared across tests.
|
|
164
|
+
#
|
|
165
|
+
# `c.before_suite` above is not a loophole. It runs once per process to
|
|
166
|
+
# configure the world, not to populate it.
|
|
167
|
+
#
|
|
168
|
+
# 2. `witness` memoizes per test, and never per process.
|
|
169
|
+
#
|
|
170
|
+
# witness(:user) { create(:user) } is memoized within a single
|
|
171
|
+
# investigation, so naming `user` five times in one test hits the database
|
|
172
|
+
# once. The moment that investigation finishes the memo is gone, and the
|
|
173
|
+
# next one builds its own.
|
|
174
|
+
#
|
|
175
|
+
# Caching across tests would be faster, and would reintroduce exactly the
|
|
176
|
+
# shared, mutable, order-dependent state that (1) exists to prevent. So the
|
|
177
|
+
# cache is scoped to the one place it is provably safe: inside a single test
|
|
178
|
+
# that owns the object outright.
|
|
179
|
+
# =============================================================================
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# .constable/config.yml
|
|
2
|
+
#
|
|
3
|
+
# Settings, as opposed to code. Everything here is a number, a flag or a glob;
|
|
4
|
+
# anything that is Ruby -- custom matchers, tier base classes, one-time global
|
|
5
|
+
# setup -- lives in test/case_helper.rb instead.
|
|
6
|
+
#
|
|
7
|
+
# Every key Constable understands is present below at its default value, so this
|
|
8
|
+
# file doubles as the complete reference. Delete anything you haven't changed;
|
|
9
|
+
# the behavior is identical either way. CLI flags win over this file for the
|
|
10
|
+
# duration of a single run.
|
|
11
|
+
|
|
12
|
+
# Glob paths to run as cold cases: original RSpec/Minitest files, driven verbatim
|
|
13
|
+
# through their own real engine, with pass/fail/timing fed into Constable's
|
|
14
|
+
# reporting, flake history and CI gate alongside native cases.
|
|
15
|
+
#
|
|
16
|
+
# This is the zero-file-change adoption route. The alternative is one line per
|
|
17
|
+
# file -- inheriting from Constable::ColdCase::RSpec or ::Minitest -- which is
|
|
18
|
+
# more explicit but touches source. Either way nothing gets rewritten.
|
|
19
|
+
#
|
|
20
|
+
# Every cold-case file is reported as a warning on every run: one per file, not
|
|
21
|
+
# one per test. Not to nag, but so "12 tests not yet under native rules" is never
|
|
22
|
+
# something the suite quietly forgets to mention.
|
|
23
|
+
cold_cases: []
|
|
24
|
+
# cold_cases:
|
|
25
|
+
# - spec/controllers/**/*_spec.rb
|
|
26
|
+
# - test/legacy/**/*_test.rb
|
|
27
|
+
|
|
28
|
+
# The blotter: flake history, the jail docket, warrants.
|
|
29
|
+
#
|
|
30
|
+
# Constable owns this store outright. It is deliberately not the app's own
|
|
31
|
+
# database, for two reasons. Native cases run inside a transaction that gets
|
|
32
|
+
# rolled back, so writing "this test just failed" through that connection would
|
|
33
|
+
# roll the record back too. And :unit-tier runs skip booting the database
|
|
34
|
+
# entirely for speed -- requiring a live server to record a pass would undo that.
|
|
35
|
+
#
|
|
36
|
+
# SQLite in WAL mode handles the real workload (a handful of tables, one row per
|
|
37
|
+
# test per run) comfortably. Point it at Postgres or MySQL only if you genuinely
|
|
38
|
+
# need one queryable store shared across many CI machines -- and always at a
|
|
39
|
+
# separate database from the app's own, never its test connection.
|
|
40
|
+
storage:
|
|
41
|
+
adapter: sqlite # sqlite (default) | postgres | mysql
|
|
42
|
+
path: .constable/constable.sqlite3 # sqlite only
|
|
43
|
+
url: # postgres/mysql only, e.g.
|
|
44
|
+
# postgres://user:pass@host/constable_metadata
|
|
45
|
+
|
|
46
|
+
warrants: false # opt-in flaky detector -- rerun a failure in isolation before believing it
|
|
47
|
+
warrant_retries: 5 # reruns before declaring a warrant (flaky) or a genuine failure
|
|
48
|
+
auto_relink: false # auto-confirm high-confidence rename detection instead of suggesting it
|
|
49
|
+
|
|
50
|
+
parole_period: 10 # consecutive clean runs before a paroled test is auto-released
|
|
51
|
+
|
|
52
|
+
coverage: false # record coverage during test runs
|
|
53
|
+
coverage_threshold: 90 # diff-based -- only lines changed in the current diff are held to it
|
|
54
|
+
coverage_html: false # generate a browsable report by default
|
|
55
|
+
|
|
56
|
+
fail_on_warnings: false # CI: fail the build when the warning count doesn't trend down
|
|
57
|
+
|
|
58
|
+
parallel_workers: auto # or an explicit integer
|
|
59
|
+
|
|
60
|
+
# Fallback path-based tier inference, used only when a case doesn't inherit from
|
|
61
|
+
# a tiered base class. The base classes in test/case_helper.rb are the primary
|
|
62
|
+
# mechanism and always win -- this is here so an un-migrated file still lands in
|
|
63
|
+
# a sensible tier rather than the slowest one.
|
|
64
|
+
tiers:
|
|
65
|
+
unit: "test/cases/models/**/*"
|
|
66
|
+
integration: "test/cases/controllers/**/*"
|
|
67
|
+
system: "test/cases/system/**/*"
|