minitest-strict 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ab7f3cff1adfd1e6a69afec2832299ca911bf2732cb3c205c9c116edfaf094eb
4
+ data.tar.gz: 8f45b484e5c08cd7c25498c028ff3f00540ef5b494561adbe964917fb2ebff87
5
+ SHA512:
6
+ metadata.gz: 2e7508a3ff72d562ca069ea2fa8a5b9330ff6066121beb8f4162849673cac097dbb2cceba10b8a03884a90285f11924846763bd834c1b56029bf87240e76544e
7
+ data.tar.gz: bef6939ad19d6ecfea699367bbfd51f69e9a0fee664927f14ff254d0efe349e0403443c5c34804d290810fbe7974c687007958e547ae646c40bb590c038fa8bd
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-02-26
9
+
10
+ ### Added
11
+
12
+ - `assert_true` / `refute_true` — assert a value is literally `true`, not just truthy
13
+ - `assert_false` / `refute_false` — assert a value is literally `false`, not just falsey
14
+ - `assert_eql` / `refute_eql` — assert equality using `eql?` instead of `==`
15
+ - Strict `assert_predicate` / `refute_predicate` — require predicates to return `true` or `false`
16
+ - Strict `assert_operator` / `refute_operator` — require operators to return `true` or `false`
17
+ - Strict `assert_nil` / `refute_nil` — use `equal?` identity check instead of `nil?`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Erik Berlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # `minitest-strict`
2
+
3
+ [![Test](https://github.com/sferik/minitest-strict/actions/workflows/test.yml/badge.svg)](https://github.com/sferik/minitest-strict/actions/workflows/test.yml)
4
+ [![Lint](https://github.com/sferik/minitest-strict/actions/workflows/lint.yml/badge.svg)](https://github.com/sferik/minitest-strict/actions/workflows/lint.yml)
5
+ [![RDoc Coverage](https://github.com/sferik/minitest-strict/actions/workflows/rdoc_coverage.yml/badge.svg)](https://github.com/sferik/minitest-strict/actions/workflows/rdoc_coverage.yml)
6
+ [![Mutant](https://github.com/sferik/minitest-strict/actions/workflows/mutant.yml/badge.svg)](https://github.com/sferik/minitest-strict/actions/workflows/mutant.yml)
7
+ [![Steep](https://github.com/sferik/minitest-strict/actions/workflows/steep.yml/badge.svg)](https://github.com/sferik/minitest-strict/actions/workflows/steep.yml)
8
+ [![Gem Version](https://badge.fury.io/rb/minitest-strict.svg)](https://badge.fury.io/rb/minitest-strict)
9
+
10
+ #### Strict assertions for [Minitest](https://github.com/minitest/minitest).
11
+
12
+ ## Motivation
13
+
14
+ Minitest's built-in assertions are lenient in ways that can mask bugs:
15
+
16
+ - `assert`, `assert_predicate`, and `assert_operator` pass for any truthy return value, not just `true`. A predicate method that accidentally returns `1`, `"yes"`, or an object will silently pass.
17
+ - `refute`, `refute_predicate`, and `refute_operator` pass for any falsey return value, not just `false`. A method that returns `nil` instead of `false` won't be caught.
18
+ - `assert_nil` and `refute_nil` call `nil?`, which can be overridden on an object and mask bugs.
19
+ - There's no built-in assertion that a value is exactly `true` or exactly `false`.
20
+ - There's no built-in assertion for `eql?` equality (e.g. distinguishing `1` from `1.0`).
21
+
22
+ `minitest-strict` fixes all of this. It redefines several Minitest assertions to require strict boolean return values and adds `assert_true`, `assert_false`, and `assert_eql` (with corresponding refutations).
23
+
24
+ Strict assertions also make mutation testing with [Mutant](https://github.com/mbj/mutant) more effective. When assertions accept only exact boolean values, mutations like replacing `true` with `false` or swapping `nil` for `false` are reliably caught — mutations that lenient assertions would let survive.
25
+
26
+ ## Installation
27
+
28
+ Add to your Gemfile:
29
+
30
+ ```ruby
31
+ gem "minitest-strict"
32
+ ```
33
+
34
+ Then require it in your test helper:
35
+
36
+ ```ruby
37
+ require "minitest/autorun"
38
+ require "minitest/strict"
39
+ ```
40
+
41
+ ## New Assertions
42
+
43
+ ### `assert_true` / `refute_true`
44
+
45
+ Passes only when the value is exactly `true` — not merely truthy.
46
+
47
+ ```ruby
48
+ assert_true true # pass
49
+ assert_true 1 # fail
50
+ assert_true "yes" # fail
51
+ assert_true nil # fail
52
+
53
+ refute_true false # pass
54
+ refute_true nil # pass
55
+ refute_true 1 # pass
56
+ refute_true true # fail
57
+ ```
58
+
59
+ ### `assert_false` / `refute_false`
60
+
61
+ Passes only when the value is exactly `false` — not merely falsey.
62
+
63
+ ```ruby
64
+ assert_false false # pass
65
+ assert_false nil # fail
66
+ assert_false 0 # fail
67
+ assert_false "" # fail
68
+
69
+ refute_false true # pass
70
+ refute_false nil # pass
71
+ refute_false 0 # pass
72
+ refute_false false # fail
73
+ ```
74
+
75
+ ### `assert_eql` / `refute_eql`
76
+
77
+ > [!NOTE]
78
+ > `1 == 1.0` is `true` in Ruby, but `1.eql?(1.0)` is `false`. Be mindful of this distinction when comparing numeric values.
79
+
80
+ Uses `eql?` instead of `==` for a stricter equality check. This distinguishes values that are `==` but not the same type.
81
+
82
+ ```ruby
83
+ assert_eql 1, 1 # pass
84
+ assert_eql "foo", "foo" # pass
85
+ assert_eql 1, 1.0 # fail — 1 == 1.0 is true, but 1.eql?(1.0) is false
86
+
87
+ refute_eql 1, 1.0 # pass
88
+ refute_eql 1, 2 # pass
89
+ refute_eql 1, 1 # fail
90
+ ```
91
+
92
+ ## Strict Redefinitions
93
+
94
+ > [!IMPORTANT]
95
+ > The following Minitest assertions are **redefined** to require exact boolean return values. Existing tests may fail if the methods under test return truthy/falsey values instead of `true`/`false`.
96
+
97
+ ### `assert_predicate` / `refute_predicate`
98
+
99
+ > [!WARNING]
100
+ > Methods like `Numeric#nonzero?` return `self` or `nil` instead of `true` or `false`. These will fail with minitest-strict's `assert_predicate` and `refute_predicate`.
101
+
102
+ Standard Minitest accepts any truthy/falsey return value. minitest-strict requires predicates to return exactly `true` or `false`.
103
+
104
+ ```ruby
105
+ assert_predicate "", :empty? # pass — String#empty? returns true
106
+ assert_predicate "hello", :empty? # fail — returns false
107
+
108
+ # Catches methods that return truthy values other than true:
109
+ assert_predicate 1, :nonzero? # fail — returns 1, not true
110
+
111
+ refute_predicate "hello", :empty? # pass — returns false
112
+ refute_predicate "", :empty? # fail — returns true
113
+
114
+ # Catches methods that return falsey values other than false:
115
+ refute_predicate 0, :nonzero? # fail — returns nil, not false
116
+ ```
117
+
118
+ ### `assert_operator` / `refute_operator`
119
+
120
+ Requires operators to return exactly `true` or `false`.
121
+
122
+ ```ruby
123
+ assert_operator 1, :<, 2 # pass — returns true
124
+ assert_operator 2, :<, 1 # fail — returns false
125
+
126
+ refute_operator 2, :<, 1 # pass — returns false
127
+ refute_operator 1, :<, 2 # fail — returns true
128
+
129
+ # Catches operators that return non-boolean values:
130
+ obj.define_singleton_method(:<=>) { |_| 1 }
131
+ assert_operator obj, :<=>, 2 # fail — returns 1, not true
132
+ ```
133
+
134
+ ### `assert_nil` / `refute_nil`
135
+
136
+ > [!NOTE]
137
+ > The standard Minitest implementation calls `nil?`, which can be overridden. minitest-strict uses `equal?` (identity) instead, so only the actual `nil` object passes.
138
+
139
+ Uses `equal?` (identity) instead of `nil?`, so objects that override `nil?` can't fool the check.
140
+
141
+ ```ruby
142
+ assert_nil nil # pass
143
+ assert_nil false # fail
144
+
145
+ # Can't be tricked by overriding nil?
146
+ obj.define_singleton_method(:nil?) { true }
147
+ assert_nil obj # fail — obj is not nil
148
+
149
+ refute_nil 1 # pass
150
+ refute_nil false # pass
151
+ refute_nil nil # fail
152
+ ```
153
+
154
+ ## License
155
+
156
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,8 @@
1
+ module Minitest # :nodoc:
2
+ ##
3
+ # Version information for minitest-strict.
4
+ module Strict
5
+ # Current version of minitest-strict.
6
+ VERSION = "1.0.0".freeze
7
+ end
8
+ end
@@ -0,0 +1,141 @@
1
+ require "minitest/assertions"
2
+ require_relative "strict/version"
3
+
4
+ ##
5
+ # Minitest extensions for strict boolean assertions.
6
+
7
+ module Minitest
8
+ ##
9
+ # Strict assertion methods that require literal +true+ or +false+
10
+ # return values instead of truthy or falsey.
11
+ module Assertions
12
+ ##
13
+ # Fails unless +obj+ is literally +true+ (not just truthy).
14
+
15
+ def assert_true obj, msg = nil
16
+ msg = message(msg) { "Expected #{mu_pp obj} to be true" }
17
+ assert obj.equal?(true), msg
18
+ end
19
+
20
+ ##
21
+ # Fails unless +obj+ is literally +false+ (not just falsey).
22
+
23
+ def assert_false obj, msg = nil
24
+ msg = message(msg) { "Expected #{mu_pp obj} to be false" }
25
+ assert obj.equal?(false), msg
26
+ end
27
+
28
+ ##
29
+ # Fails if +obj+ is literally +true+.
30
+
31
+ def refute_true obj, msg = nil
32
+ msg = message(msg) { "Expected true to not be true" }
33
+ refute obj.equal?(true), msg
34
+ end
35
+
36
+ ##
37
+ # Fails if +obj+ is literally +false+.
38
+
39
+ def refute_false obj, msg = nil
40
+ msg = message(msg) { "Expected false to not be false" }
41
+ refute obj.equal?(false), msg
42
+ end
43
+
44
+ ##
45
+ # Fails unless +act+ is eql? to +exp+. Uses +Object#eql?+ for
46
+ # equality without type coercion. Eg:
47
+ #
48
+ # assert_eql 1, 1 # pass
49
+ # assert_eql 1, 1.0 # fail
50
+
51
+ def assert_eql exp, act, msg = nil
52
+ msg = message(msg) { "Expected #{mu_pp act} to be eql? to #{mu_pp exp}" }
53
+ assert exp.eql?(act), msg
54
+ end
55
+
56
+ ##
57
+ # Fails if +act+ is eql? to +exp+.
58
+
59
+ def refute_eql exp, act, msg = nil
60
+ msg = message(msg) { "Expected #{mu_pp act} to not be eql? to #{mu_pp exp}" }
61
+ refute exp.eql?(act), msg
62
+ end
63
+
64
+ # Strict redefinitions -- require boolean return values
65
+
66
+ silence = $VERBOSE
67
+ $VERBOSE = nil
68
+
69
+ ##
70
+ # Fails unless +o1+ is +op+. Requires +op+ to return literal
71
+ # +true+, not just a truthy value.
72
+ #
73
+ # assert_predicate str, :empty?
74
+
75
+ def assert_predicate o1, op, msg = nil
76
+ assert_respond_to o1, op, include_all: true
77
+ msg = message(msg) { "Expected #{mu_pp o1} to be #{op}" }
78
+ assert_equal true, o1.__send__(op), msg
79
+ end
80
+
81
+ ##
82
+ # Fails if +o1+ is +op+. Requires +op+ to return literal
83
+ # +false+, not just a falsey value.
84
+ #
85
+ # refute_predicate str, :empty?
86
+
87
+ def refute_predicate o1, op, msg = nil
88
+ assert_respond_to o1, op, include_all: true
89
+ msg = message(msg) { "Expected #{mu_pp o1} to not be #{op}" }
90
+ assert_equal false, o1.__send__(op), msg
91
+ end
92
+
93
+ ##
94
+ # For testing with binary operators. Requires the operator to
95
+ # return literal +true+, not just a truthy value. Falls through
96
+ # to assert_predicate if +o2+ is not given. Eg:
97
+ #
98
+ # assert_operator 5, :<=, 4
99
+
100
+ def assert_operator o1, op, o2 = UNDEFINED, msg = nil
101
+ return assert_predicate o1, op, msg if o2 == UNDEFINED
102
+
103
+ assert_respond_to o1, op, include_all: true
104
+ msg = message(msg) { "Expected #{mu_pp o1} to be #{op} #{mu_pp o2}" }
105
+ assert_equal true, o1.__send__(op, o2), msg
106
+ end
107
+
108
+ ##
109
+ # For testing with binary operators. Requires the operator to
110
+ # return literal +false+, not just a falsey value. Falls through
111
+ # to refute_predicate if +o2+ is not given. Eg:
112
+ #
113
+ # refute_operator 1, :>, 2
114
+
115
+ def refute_operator o1, op, o2 = UNDEFINED, msg = nil
116
+ return refute_predicate o1, op, msg if o2 == UNDEFINED
117
+
118
+ assert_respond_to o1, op, include_all: true
119
+ msg = message(msg) { "Expected #{mu_pp o1} to not be #{op} #{mu_pp o2}" }
120
+ assert_equal false, o1.__send__(op, o2), msg
121
+ end
122
+
123
+ ##
124
+ # Fails unless +obj+ is nil. Uses +equal?+ for identity check.
125
+
126
+ def assert_nil obj, msg = nil
127
+ msg = message(msg) { "Expected #{mu_pp obj} to be nil" }
128
+ assert obj.equal?(nil), msg
129
+ end
130
+
131
+ ##
132
+ # Fails if +obj+ is nil. Uses +equal?+ for identity check.
133
+
134
+ def refute_nil obj, msg = nil
135
+ msg = message(msg) { "Expected nil to not be nil" }
136
+ refute obj.equal?(nil), msg
137
+ end
138
+
139
+ $VERBOSE = silence
140
+ end
141
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest-strict
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Erik Berlin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.21'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '7'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '5.21'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '7'
32
+ description: Redefines Minitest assertions to require strict boolean return values
33
+ and adds assert_true, assert_false, and assert_eql.
34
+ email:
35
+ - sferik@gmail.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - CHANGELOG.md
41
+ - LICENSE.txt
42
+ - README.md
43
+ - lib/minitest/strict.rb
44
+ - lib/minitest/strict/version.rb
45
+ homepage: https://github.com/sferik/minitest-strict
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ rubygems_mfa_required: 'true'
50
+ source_code_uri: https://github.com/sferik/minitest-strict
51
+ bug_tracker_uri: https://github.com/sferik/minitest-strict/issues
52
+ changelog_uri: https://github.com/sferik/minitest-strict/blob/main/CHANGELOG.md
53
+ documentation_uri: https://www.rubydoc.info/gems/minitest-strict
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 4.0.6
69
+ specification_version: 4
70
+ summary: Strict assertions for Minitest
71
+ test_files: []