mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Compare commits
115 commits
7e09016e5c
...
d74a772f43
Author | SHA1 | Date | |
---|---|---|---|
|
d74a772f43 | ||
|
0e3f626932 | ||
|
d45d5d4479 | ||
|
4a630b1ebf | ||
|
d72895fe10 | ||
|
04f151fddf | ||
|
9cbb5d2cf7 | ||
|
3852606b28 | ||
|
726a2e1515 | ||
|
5c08427ca0 | ||
|
735122a94b | ||
|
9ea5c261b1 | ||
|
24a860ea11 | ||
|
528ad7257d | ||
|
7149ef7df5 | ||
|
cb89589155 | ||
|
a5e8f11e11 | ||
|
abbd6ffd71 | ||
|
fd372226ab | ||
|
6a5e5b8f7a | ||
|
4a0bfc1cb2 | ||
|
d46698d81a | ||
|
8c3900adcb | ||
|
30602663fe | ||
|
b8901f522a | ||
|
c4bcf54b98 | ||
|
acf810553a | ||
|
faff2933e6 | ||
|
0f8c46d6ef | ||
|
7620f58fb8 | ||
|
feaf1c6015 | ||
|
8f80b10fc1 | ||
|
a3c55dfa47 | ||
|
fa99987780 | ||
|
d378583054 | ||
|
6255cc85c4 | ||
|
e6584c9f04 | ||
|
f55c60e01f | ||
|
4b68b8e3de | ||
|
c3e7edc700 | ||
|
149c0e6e4b | ||
|
9f54a9e542 | ||
|
65a4b8e756 | ||
|
b52593dbde | ||
|
7e2ec4ee37 | ||
|
952e949307 | ||
|
293faccd5c | ||
|
2985ef5919 | ||
|
bd44b5562e | ||
|
47a62ece78 | ||
|
7ffa63718b | ||
|
275b217c6c | ||
|
fbe877690d | ||
|
a967dce241 | ||
|
1f98bf9ff1 | ||
|
5f499336ac | ||
|
df10c8e75b | ||
|
a585ef0996 | ||
|
2d6c8844d4 | ||
|
321c15407d | ||
|
c256ef763e | ||
|
8efd38fbdd | ||
|
015d36ea4c | ||
|
318e4eba89 | ||
|
e2cdc9e08e | ||
|
60b5f151f1 | ||
|
8b12262c62 | ||
|
6e7d215f69 | ||
|
12eb2e9357 | ||
|
1093571fbd | ||
|
c00d2fe4e6 | ||
|
a6149b2671 | ||
|
4906dfae0d | ||
|
24fd7d1e91 | ||
|
baff1de1d8 | ||
|
4dacaab6dc | ||
|
a31ffe3fa3 | ||
|
c77da67341 | ||
|
8959d28b38 | ||
|
39e4f8e37a | ||
|
e2130d12d3 | ||
|
0177a678f9 | ||
|
a728a037d4 | ||
|
163f94287e | ||
|
e38e3ecc32 | ||
|
70d0009db5 | ||
|
d9082dab45 | ||
|
b3aa2d62c0 | ||
|
c6afa0adb3 | ||
|
bc0a9c03c9 | ||
|
11e227b29f | ||
|
8e83edcc35 | ||
|
090c95b162 | ||
|
2516803b0d | ||
|
e9d3f31ac3 | ||
|
5c910e5a85 | ||
|
25b9931002 | ||
|
422b0efa59 | ||
|
c1e1666449 | ||
|
4dfa5ccb6e | ||
|
1998edbbb2 | ||
|
079272c9de | ||
|
ccdf9f124b | ||
|
7549351cce | ||
|
0505f210f9 | ||
|
9d6d8de72f | ||
|
027521a7bc | ||
|
d10531430c | ||
|
b5c61f9003 | ||
|
17a3ca3ac7 | ||
|
02027cda53 | ||
|
18e9c1c35d | ||
|
95764140ee | ||
|
61dee8d7db | ||
|
f4c5caa656 |
119 changed files with 2728 additions and 2451 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -10,3 +10,5 @@
|
|||
|
||||
# Ignore JUnit output
|
||||
output.xml
|
||||
|
||||
/test.cr
|
||||
|
|
|
@ -13,7 +13,7 @@ before_script:
|
|||
|
||||
spec:
|
||||
script:
|
||||
- crystal spec --error-on-warnings --junit_output=. spec/runtime_example_spec.cr spec/matchers/ spec/spectator/*.cr
|
||||
- crystal spec --error-on-warnings --junit_output=. spec/matchers/ spec/spectator/*.cr
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
|
|
75
CHANGELOG.md
75
CHANGELOG.md
|
@ -4,7 +4,70 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.11.7] - 2023-10-16
|
||||
### Fixed
|
||||
- Fix memoized value (`let`) with a union type causing segfault. [#81](https://gitlab.com/arctic-fox/spectator/-/issues/81)
|
||||
|
||||
## [0.11.6] - 2023-01-26
|
||||
### Added
|
||||
- Added ability to cast types using the return value from expect/should statements with a type matcher.
|
||||
- Added support for string interpolation in context names/labels.
|
||||
|
||||
### Fixed
|
||||
- Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. [#51](https://github.com/icy-arctic-fox/spectator/issues/51)
|
||||
- Fix malformed method signature when using named splat with keyword arguments in mocked type. [#49](https://github.com/icy-arctic-fox/spectator/issues/49)
|
||||
|
||||
### Changed
|
||||
- Expectations using 'should' syntax report file and line where the 'should' keyword is instead of the test start.
|
||||
- Add non-captured block argument in preparation for Crystal 1.8.0.
|
||||
|
||||
## [0.11.5] - 2022-12-18
|
||||
### Added
|
||||
- Added support for mock modules and types that include mocked modules.
|
||||
|
||||
### Fixed
|
||||
- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
|
||||
- Fix method stubs used on methods that capture blocks.
|
||||
- Fix type name resolution for when using custom types in a mocked typed.
|
||||
- Prevent comparing range arguments with non-compatible types in stubs. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
|
||||
|
||||
### Changed
|
||||
- Simplify string representation of mock-related types.
|
||||
- Remove unnecessary redefinitions of methods when adding stub functionality to a type.
|
||||
- Allow metadata to be stored as nil to reduce overhead when tracking nodes without tags.
|
||||
- Use normal equality (==) instead of case-equality (===) with proc arguments in stubs.
|
||||
- Change stub value cast logic to avoid compiler bug. [#80](https://gitlab.com/arctic-fox/spectator/-/issues/80)
|
||||
|
||||
## [0.11.4] - 2022-11-27
|
||||
### Added
|
||||
- Add support for using named (keyword) arguments in place of positional arguments in stubs. [#47](https://github.com/icy-arctic-fox/spectator/issues/47)
|
||||
- Add `before`, `after`, and `around` as aliases for `before_each`, `after_each`, and `around_each` respectively.
|
||||
|
||||
### Fixed
|
||||
- Clear stubs defined with `expect().to receive()` syntax after test finishes to prevent leakage between tests.
|
||||
- Ensure stubs defined with `allow().to receive()` syntax are cleared after test finishes when used inside a test (another leakage).
|
||||
- Fix crash caused when logging is enabled after running an example that attempts to exit.
|
||||
|
||||
### Removed
|
||||
- Removed support for stubbing undefined (untyped) methods in lazy doubles. Avoids possible segfault.
|
||||
|
||||
## [0.11.3] - 2022-09-03
|
||||
### Fixed
|
||||
- Display error block (failure message and stack trace) when using `fail`. [#78](https://gitlab.com/arctic-fox/spectator/-/issues/78)
|
||||
- Defining a custom matcher outside of the `Spectator` namespace no longer produces a compilation error. [#46](https://github.com/icy-arctic-fox/spectator/issues/46)
|
||||
|
||||
## [0.11.2] - 2022-08-07
|
||||
### Fixed
|
||||
- `expect_raises` with block and no arguments produces compilation error. [#77](https://gitlab.com/arctic-fox/spectator/-/issues/77)
|
||||
|
||||
### Changed
|
||||
- `-e` (`--example`) CLI option performs a partial match instead of exact match. [#71](https://gitlab.com/arctic-fox/spectator/-/issues/71) [#45](https://github.com/icy-arctic-fox/spectator/issues/45)
|
||||
|
||||
## [0.11.1] - 2022-07-18
|
||||
### Fixed
|
||||
- Workaround nilable type issue with memoized value. [#76](https://gitlab.com/arctic-fox/spectator/-/issues/76)
|
||||
|
||||
## [0.11.0] - 2022-07-14
|
||||
### Changed
|
||||
- Overhauled mock and double system. [#63](https://gitlab.com/arctic-fox/spectator/-/issues/63)
|
||||
- Testing if `exit` is called no longer is done with stubs and catching the `Spectator::SystemExit` exception should be caught. [#29](https://github.com/icy-arctic-fox/spectator/issues/29)
|
||||
|
@ -387,7 +450,15 @@ This has been changed so that it compiles and raises an error at runtime with a
|
|||
First version ready for public use.
|
||||
|
||||
|
||||
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.6...master
|
||||
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...master
|
||||
[0.11.7]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...v0.11.7
|
||||
[0.11.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...v0.11.6
|
||||
[0.11.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...v0.11.5
|
||||
[0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4
|
||||
[0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.2...v0.11.3
|
||||
[0.11.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.1...v0.11.2
|
||||
[0.11.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...v0.11.1
|
||||
[0.11.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.6...v0.11.0
|
||||
[0.10.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.5...v0.10.6
|
||||
[0.10.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.4...v0.10.5
|
||||
[0.10.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.3...v0.10.4
|
||||
|
|
10
README.md
10
README.md
|
@ -287,7 +287,7 @@ Spectator.describe Driver do
|
|||
# Call the mock method.
|
||||
subject.do_something(interface, dbl)
|
||||
# Verify everything went okay.
|
||||
expect(interface).to have_received(:invoke).with(thing)
|
||||
expect(interface).to have_received(:invoke).with(dbl)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
@ -360,10 +360,10 @@ Items not marked as completed may have partial implementations.
|
|||
- [X] Mocks (Stub real types) - `mock TYPE { }`
|
||||
- [X] Doubles (Stand-ins for real types) - `double NAME { }`
|
||||
- [X] Method stubs - `allow().to receive()`, `allow().to receive().and_return()`
|
||||
- [X] Spies - `expect().to have_receive()`
|
||||
- [ ] Message expectations - `expect().to receive().at_least()`
|
||||
- [X] Argument expectations - `expect().to receive().with()`
|
||||
- [ ] Message ordering - `expect().to receive().ordered`
|
||||
- [X] Spies - `expect().to have_received()`
|
||||
- [X] Message expectations - `expect().to have_received().at_least()`
|
||||
- [X] Argument expectations - `expect().to have_received().with()`
|
||||
- [ ] Message ordering - `expect().to have_received().ordered`
|
||||
- [X] Null doubles
|
||||
- [X] Runner
|
||||
- [X] Fail fast
|
||||
|
|
12
shard.yml
12
shard.yml
|
@ -1,16 +1,20 @@
|
|||
name: spectator
|
||||
version: 0.11.0-alpha
|
||||
version: 0.11.7
|
||||
description: |
|
||||
A feature-rich spec testing framework for Crystal with similarities to RSpec.
|
||||
Feature-rich testing framework for Crystal inspired by RSpec.
|
||||
|
||||
authors:
|
||||
- Michael Miller <icy.arctic.fox@gmail.com>
|
||||
|
||||
crystal: 1.5.0
|
||||
crystal: ">= 1.6.0, < 1.11"
|
||||
|
||||
license: MIT
|
||||
|
||||
dependencies:
|
||||
mocks:
|
||||
github: icy-arctic-fox/mocks
|
||||
|
||||
development_dependencies:
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
version: ~> 1.0.0
|
||||
version: ~> 1.5.0
|
||||
|
|
91
spec/docs/custom_matchers_spec.cr
Normal file
91
spec/docs/custom_matchers_spec.cr
Normal file
|
@ -0,0 +1,91 @@
|
|||
require "../spec_helper"
|
||||
|
||||
# https://gitlab.com/arctic-fox/spectator/-/wikis/Custom-Matchers
|
||||
Spectator.describe "Custom Matchers Docs" do
|
||||
context "value matcher" do
|
||||
# Sub-type of Matcher to suit our needs.
|
||||
# Notice this is a struct.
|
||||
struct MultipleOfMatcher(ExpectedType) < Spectator::Matchers::ValueMatcher(ExpectedType)
|
||||
# Short text about the matcher's purpose.
|
||||
# This explains what condition satisfies the matcher.
|
||||
# The description is used when the one-liner syntax is used.
|
||||
def description : String
|
||||
"is a multiple of #{expected.label}"
|
||||
end
|
||||
|
||||
# Checks whether the matcher is satisfied with the expression given to it.
|
||||
private def match?(actual : Spectator::Expression(T)) : Bool forall T
|
||||
actual.value % expected.value == 0
|
||||
end
|
||||
|
||||
# Message displayed when the matcher isn't satisfied.
|
||||
# The message should typically only contain the test expression labels.
|
||||
private def failure_message(actual : Spectator::Expression(T)) : String forall T
|
||||
"#{actual.label} is not a multiple of #{expected.label}"
|
||||
end
|
||||
|
||||
# Message displayed when the matcher isn't satisfied and is negated.
|
||||
# This is essentially what would satisfy the matcher if it wasn't negated.
|
||||
# The message should typically only contain the test expression labels.
|
||||
private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T
|
||||
"#{actual.label} is a multiple of #{expected.label}"
|
||||
end
|
||||
end
|
||||
|
||||
# The DSL portion of the matcher.
|
||||
# This captures the test expression and creates an instance of the matcher.
|
||||
macro be_a_multiple_of(expected)
|
||||
%value = ::Spectator::Value.new({{expected}}, {{expected.stringify}})
|
||||
MultipleOfMatcher.new(%value)
|
||||
end
|
||||
|
||||
specify do
|
||||
expect(9).to be_a_multiple_of(3)
|
||||
# or negated:
|
||||
expect(5).to_not be_a_multiple_of(2)
|
||||
end
|
||||
|
||||
specify "failure messages" do
|
||||
expect { expect(9).to be_a_multiple_of(5) }.to raise_error(Spectator::ExpectationFailed, "9 is not a multiple of 5")
|
||||
expect { expect(6).to_not be_a_multiple_of(3) }.to raise_error(Spectator::ExpectationFailed, "6 is a multiple of 3")
|
||||
end
|
||||
end
|
||||
|
||||
context "standard matcher" do
|
||||
struct OddMatcher < Spectator::Matchers::StandardMatcher
|
||||
def description : String
|
||||
"is odd"
|
||||
end
|
||||
|
||||
private def match?(actual : Spectator::Expression(T)) : Bool forall T
|
||||
actual.value % 2 == 1
|
||||
end
|
||||
|
||||
private def failure_message(actual : Spectator::Expression(T)) : String forall T
|
||||
"#{actual.label} is not odd"
|
||||
end
|
||||
|
||||
private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T
|
||||
"#{actual.label} is odd"
|
||||
end
|
||||
|
||||
private def does_not_match?(actual : Spectator::Expression(T)) : Bool forall T
|
||||
actual.value % 2 == 0
|
||||
end
|
||||
end
|
||||
|
||||
macro be_odd
|
||||
OddMatcher.new
|
||||
end
|
||||
|
||||
specify do
|
||||
expect(9).to be_odd
|
||||
expect(2).to_not be_odd
|
||||
end
|
||||
|
||||
specify "failure messages" do
|
||||
expect { expect(2).to be_odd }.to raise_error(Spectator::ExpectationFailed, "2 is not odd")
|
||||
expect { expect(3).to_not be_odd }.to raise_error(Spectator::ExpectationFailed, "3 is odd")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -123,6 +123,109 @@ Spectator.describe "Mocks Docs" do
|
|||
end
|
||||
end
|
||||
|
||||
context "Mock Modules" do
|
||||
module MyModule
|
||||
def something
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
describe "#something" do
|
||||
# Define a mock for MyModule.
|
||||
mock MyClass
|
||||
|
||||
it "does something" do
|
||||
# Use mock here.
|
||||
end
|
||||
end
|
||||
|
||||
module MyFileUtils
|
||||
def self.rm_rf(path)
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
mock MyFileUtils
|
||||
|
||||
it "deletes all of my files" do
|
||||
utils = class_mock(MyFileUtils)
|
||||
allow(utils).to receive(:rm_rf)
|
||||
utils.rm_rf("/")
|
||||
expect(utils).to have_received(:rm_rf).with("/")
|
||||
end
|
||||
|
||||
module MyFileUtils2
|
||||
extend self
|
||||
|
||||
def rm_rf(path)
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
mock(MyFileUtils2) do
|
||||
# Define a default stub for the method.
|
||||
stub def self.rm_rf(path)
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
it "deletes all of my files part 2" do
|
||||
utils = class_mock(MyFileUtils2)
|
||||
allow(utils).to receive(:rm_rf)
|
||||
utils.rm_rf("/")
|
||||
expect(utils).to have_received(:rm_rf).with("/")
|
||||
end
|
||||
|
||||
module Runnable
|
||||
def run
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
mock Runnable
|
||||
|
||||
specify do
|
||||
runnable = mock(Runnable) # or new_mock(Runnable)
|
||||
runnable.run
|
||||
end
|
||||
|
||||
module Runnable2
|
||||
abstract def command : String
|
||||
|
||||
def run_one
|
||||
"Running #{command}"
|
||||
end
|
||||
end
|
||||
|
||||
mock Runnable2, command: "ls -l"
|
||||
|
||||
specify do
|
||||
runnable = mock(Runnable2)
|
||||
expect(runnable.run_one).to eq("Running ls -l")
|
||||
runnable = mock(Runnable2, command: "echo foo")
|
||||
expect(runnable.run_one).to eq("Running echo foo")
|
||||
end
|
||||
|
||||
context "Injecting Mocks" do
|
||||
module MyFileUtils
|
||||
def self.rm_rf(path)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
inject_mock MyFileUtils do
|
||||
stub def self.rm_rf(path)
|
||||
"Simulating deletion of #{path}"
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
specify do
|
||||
expect(MyFileUtils.rm_rf("/")).to be_false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "Injecting Mocks" do
|
||||
struct MyStruct
|
||||
def something
|
||||
|
@ -146,9 +249,9 @@ Spectator.describe "Mocks Docs" do
|
|||
inst.something
|
||||
end
|
||||
|
||||
it "leaks stubs to other examples" do
|
||||
it "reverts to default stub for other examples" do
|
||||
inst = mock(MyStruct)
|
||||
expect(inst.something).to eq(7) # Previous stub was leaked.
|
||||
expect(inst.something).to eq(5) # Default stub used instead of original behavior.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
require "../spec_helper"
|
||||
|
||||
private abstract class Interface
|
||||
abstract def invoke(thing) : String
|
||||
end
|
||||
module Readme
|
||||
abstract class Interface
|
||||
abstract def invoke(thing) : String
|
||||
end
|
||||
|
||||
# Type being tested.
|
||||
private class Driver
|
||||
def do_something(interface : Interface, thing)
|
||||
interface.invoke(thing)
|
||||
# Type being tested.
|
||||
class Driver
|
||||
def do_something(interface : Interface, thing)
|
||||
interface.invoke(thing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Spectator.describe Driver do
|
||||
Spectator.describe Readme::Driver do
|
||||
# Define a mock for Interface.
|
||||
mock Interface
|
||||
mock Readme::Interface
|
||||
|
||||
# Define a double that the interface will use.
|
||||
double(:my_double, foo: 42)
|
||||
|
||||
it "does a thing" do
|
||||
# Create an instance of the mock interface.
|
||||
interface = mock(Interface)
|
||||
interface = mock(Readme::Interface)
|
||||
# Indicate that `#invoke` should return "test" when called.
|
||||
allow(interface).to receive(:invoke).and_return("test")
|
||||
|
||||
|
|
70
spec/features/expect_type_spec.cr
Normal file
70
spec/features/expect_type_spec.cr
Normal file
|
@ -0,0 +1,70 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe "Expect Type", :smoke do
|
||||
context "with expect syntax" do
|
||||
it "ensures a type is cast" do
|
||||
value = 42.as(String | Int32)
|
||||
expect(value).to be_a(String | Int32)
|
||||
expect(value).to compile_as(String | Int32)
|
||||
value = expect(value).to be_a(Int32)
|
||||
expect(value).to eq(42)
|
||||
expect(value).to be_a(Int32)
|
||||
expect(value).to compile_as(Int32)
|
||||
expect(value).to_not respond_to(:downcase)
|
||||
end
|
||||
|
||||
it "ensures a type is not nil" do
|
||||
value = 42.as(Int32?)
|
||||
expect(value).to be_a(Int32?)
|
||||
expect(value).to compile_as(Int32?)
|
||||
value = expect(value).to_not be_nil
|
||||
expect(value).to eq(42)
|
||||
expect(value).to be_a(Int32)
|
||||
expect(value).to compile_as(Int32)
|
||||
expect { value.not_nil! }.to_not raise_error(NilAssertionError)
|
||||
end
|
||||
|
||||
it "removes types from a union" do
|
||||
value = 42.as(String | Int32)
|
||||
expect(value).to be_a(String | Int32)
|
||||
expect(value).to compile_as(String | Int32)
|
||||
value = expect(value).to_not be_a(String)
|
||||
expect(value).to eq(42)
|
||||
expect(value).to be_a(Int32)
|
||||
expect(value).to compile_as(Int32)
|
||||
expect(value).to_not respond_to(:downcase)
|
||||
end
|
||||
end
|
||||
|
||||
context "with should syntax" do
|
||||
it "ensures a type is cast" do
|
||||
value = 42.as(String | Int32)
|
||||
value.should be_a(String | Int32)
|
||||
value = value.should be_a(Int32)
|
||||
value.should eq(42)
|
||||
value.should be_a(Int32)
|
||||
value.should compile_as(Int32)
|
||||
value.should_not respond_to(:downcase)
|
||||
end
|
||||
|
||||
it "ensures a type is not nil" do
|
||||
value = 42.as(Int32?)
|
||||
value.should be_a(Int32?)
|
||||
value = value.should_not be_nil
|
||||
value.should eq(42)
|
||||
value.should be_a(Int32)
|
||||
value.should compile_as(Int32)
|
||||
expect { value.not_nil! }.to_not raise_error(NilAssertionError)
|
||||
end
|
||||
|
||||
it "removes types from a union" do
|
||||
value = 42.as(String | Int32)
|
||||
value.should be_a(String | Int32)
|
||||
value = value.should_not be_a(String)
|
||||
value.should eq(42)
|
||||
value.should be_a(Int32)
|
||||
value.should compile_as(Int32)
|
||||
value.should_not respond_to(:downcase)
|
||||
end
|
||||
end
|
||||
end
|
22
spec/features/interpolated_label_spec.cr
Normal file
22
spec/features/interpolated_label_spec.cr
Normal file
|
@ -0,0 +1,22 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe "Interpolated Label", :smoke do
|
||||
let(foo) { "example" }
|
||||
let(bar) { "context" }
|
||||
|
||||
it "interpolates #{foo} labels" do |example|
|
||||
expect(example.name).to eq("interpolates example labels")
|
||||
end
|
||||
|
||||
context "within a #{bar}" do
|
||||
let(foo) { "multiple" }
|
||||
|
||||
it "interpolates context labels" do |example|
|
||||
expect(example.group.name).to eq("within a context")
|
||||
end
|
||||
|
||||
it "interpolates #{foo} levels" do |example|
|
||||
expect(example.name).to eq("interpolates multiple levels")
|
||||
end
|
||||
end
|
||||
end
|
0
spec/helpers/.gitkeep
Normal file
0
spec/helpers/.gitkeep
Normal file
|
@ -1,71 +0,0 @@
|
|||
require "ecr"
|
||||
require "json"
|
||||
require "./result"
|
||||
|
||||
module Spectator::SpecHelpers
|
||||
# Wrapper for compiling and running an example at runtime and getting a result.
|
||||
class Example
|
||||
# Creates the example.
|
||||
# The *spec_helper_path* is the path to spec_helper.cr file.
|
||||
# The name or ID of the example is given by *example_id*.
|
||||
# Lastly, the source code for the example is given by *example_code*.
|
||||
def initialize(@spec_helper_path : String, @example_id : String, @example_code : String)
|
||||
end
|
||||
|
||||
# Instructs the Crystal compiler to compile the test.
|
||||
# Returns an instance of `JSON::Any`.
|
||||
# This will be the outcome and information about the test.
|
||||
# Output will be suppressed for the test.
|
||||
# If an error occurs while attempting to compile and run the test, an error will be raised.
|
||||
def compile
|
||||
# Create a temporary file containing the test.
|
||||
with_tempfile do |source_file|
|
||||
args = ["run", "--no-color", source_file, "--", "--json"]
|
||||
Process.run(crystal_executable, args) do |process|
|
||||
JSON.parse(process.output)
|
||||
rescue JSON::ParseException
|
||||
raise "Compilation of example #{@example_id} failed\n\n#{process.error.gets_to_end}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Same as `#compile`, but returns the result of the first example in the test.
|
||||
# Returns a `SpectatorHelpers::Result` instance.
|
||||
def result
|
||||
output = compile
|
||||
example = output["examples"][0]
|
||||
Result.from_json_any(example)
|
||||
end
|
||||
|
||||
# Constructs the string representation of the example.
|
||||
# This produces the Crystal source code.
|
||||
# *io* is the file handle to write to.
|
||||
# The *dir* is the directory of the file being written to.
|
||||
# This is needed to resolve the relative path to the spec_helper.cr file.
|
||||
private def write(io, dir)
|
||||
spec_helper_path = Path[@spec_helper_path].relative_to(dir) # ameba:disable Lint/UselessAssign
|
||||
ECR.embed(__DIR__ + "/example.ecr", io)
|
||||
end
|
||||
|
||||
# Creates a temporary file containing the compilable example code.
|
||||
# Yields the path of the temporary file.
|
||||
# Ensures the file is deleted after it is done being used.
|
||||
private def with_tempfile
|
||||
tempfile = File.tempfile("_#{@example_id}_spec.cr") do |file|
|
||||
dir = File.dirname(file.path)
|
||||
write(file, dir)
|
||||
end
|
||||
|
||||
begin
|
||||
yield tempfile.path
|
||||
ensure
|
||||
tempfile.delete
|
||||
end
|
||||
end
|
||||
|
||||
# Attempts to find the Crystal compiler on the system or raises an error.
|
||||
private def crystal_executable
|
||||
Process.find_executable("crystal") || raise("Could not find Crystal compiler")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
require "<%= spec_helper_path %>"
|
||||
|
||||
Spectator.describe "<%= @example_id %>" do
|
||||
<%= @example_code %>
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
module Spectator::SpecHelpers
|
||||
# Information about an `expect` call in an example.
|
||||
struct Expectation
|
||||
# Indicates whether the expectation passed or failed.
|
||||
getter? satisfied : Bool
|
||||
|
||||
# Message when the expectation failed.
|
||||
# Only available when `#satisfied?` is false.
|
||||
getter! message : String
|
||||
|
||||
# Additional information about the expectation.
|
||||
# Only available when `#satisfied?` is false.
|
||||
getter! values : Hash(String, String)
|
||||
|
||||
# Creates the expectation outcome.
|
||||
def initialize(@satisfied, @message, @values)
|
||||
end
|
||||
|
||||
# Extracts the expectation information from a `JSON::Any` object.
|
||||
def self.from_json_any(object : JSON::Any)
|
||||
satisfied = object["satisfied"].as_bool
|
||||
message = object["failure"]?.try(&.as_s?)
|
||||
values = object["values"]?.try(&.as_h?)
|
||||
values = values.transform_values(&.as_s) if values
|
||||
new(satisfied, message, values)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,67 +0,0 @@
|
|||
module Spectator::SpecHelpers
|
||||
# Information about an example compiled and run at runtime.
|
||||
struct Result
|
||||
# Status of the example after running.
|
||||
enum Outcome
|
||||
Success
|
||||
Failure
|
||||
Error
|
||||
Unknown
|
||||
end
|
||||
|
||||
# Full name and description of the example.
|
||||
getter name : String
|
||||
|
||||
# Status of the example after running.
|
||||
getter outcome : Outcome
|
||||
|
||||
# List of expectations ran in the example.
|
||||
getter expectations : Array(Expectation)
|
||||
|
||||
# Creates the result.
|
||||
def initialize(@name, @outcome, @expectations)
|
||||
end
|
||||
|
||||
# Checks if the example was successful.
|
||||
def success?
|
||||
outcome.success?
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def successful?
|
||||
outcome.success?
|
||||
end
|
||||
|
||||
# Checks if the example failed, but did not error.
|
||||
def failure?
|
||||
outcome.failure?
|
||||
end
|
||||
|
||||
# Checks if the example encountered an error.
|
||||
def error?
|
||||
outcome.error?
|
||||
end
|
||||
|
||||
# Extracts the result information from a `JSON::Any` object.
|
||||
def self.from_json_any(object : JSON::Any)
|
||||
name = object["description"].as_s
|
||||
outcome = parse_outcome_string(object["status"].as_s)
|
||||
expectations = if (list = object["expectations"].as_a?)
|
||||
list.map { |e| Expectation.from_json_any(e) }
|
||||
else
|
||||
[] of Expectation
|
||||
end
|
||||
new(name, outcome, expectations)
|
||||
end
|
||||
|
||||
# Converts a result string, such as "fail" to an enum value.
|
||||
private def self.parse_outcome_string(string)
|
||||
case string
|
||||
when /pass/i then Outcome::Success
|
||||
when /fail/i then Outcome::Failure
|
||||
when /error/i then Outcome::Error
|
||||
else Outcome::Unknown
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,12 +9,32 @@ Spectator.describe "GitHub Issue #44" do
|
|||
let(command) { "ls -l" }
|
||||
let(exception) { File::NotFoundError.new("File not found", file: "test.file") }
|
||||
|
||||
before_each do
|
||||
expect(Process).to receive(:run).with(command, shell: true, output: :pipe).and_raise(exception)
|
||||
context "with positional arguments" do
|
||||
before_each do
|
||||
pipe = Process::Redirect::Pipe
|
||||
expect(Process).to receive(:run).with(command, nil, nil, false, true, pipe, pipe, pipe, nil).and_raise(exception)
|
||||
end
|
||||
|
||||
it "must stub Process.run" do
|
||||
expect do
|
||||
Process.run(command, shell: true, output: :pipe) do |_process|
|
||||
end
|
||||
end.to raise_error(File::NotFoundError, "File not found")
|
||||
end
|
||||
end
|
||||
|
||||
skip "must stub Process.run", skip: "Method mock not applied" do
|
||||
Process.run(command, shell: true, output: :pipe) do |_process|
|
||||
# Original issue uses keyword arguments in place of positional arguments.
|
||||
context "keyword arguments in place of positional arguments" do
|
||||
before_each do
|
||||
pipe = Process::Redirect::Pipe
|
||||
expect(Process).to receive(:run).with(command, shell: true, output: pipe).and_raise(exception)
|
||||
end
|
||||
|
||||
it "must stub Process.run" do
|
||||
expect do
|
||||
Process.run(command, shell: true, output: :pipe) do |_process|
|
||||
end
|
||||
end.to raise_error(File::NotFoundError, "File not found")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
18
spec/issues/github_issue_47_spec.cr
Normal file
18
spec/issues/github_issue_47_spec.cr
Normal file
|
@ -0,0 +1,18 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe "GitHub Issue #47" do
|
||||
class Original
|
||||
def foo(arg1, arg2)
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
mock Original
|
||||
|
||||
let(fake) { mock(Original) }
|
||||
|
||||
specify do
|
||||
expect(fake).to receive(:foo).with("arg1", arg2: "arg2")
|
||||
fake.foo("arg1", "arg2")
|
||||
end
|
||||
end
|
135
spec/issues/github_issue_48_spec.cr
Normal file
135
spec/issues/github_issue_48_spec.cr
Normal file
|
@ -0,0 +1,135 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe "GitHub Issue #48" do
|
||||
class Test
|
||||
def return_this(thing : T) : T forall T
|
||||
thing
|
||||
end
|
||||
|
||||
def map(thing : T, & : T -> U) : U forall T, U
|
||||
yield thing
|
||||
end
|
||||
|
||||
def make_nilable(thing : T) : T? forall T
|
||||
thing.as(T?)
|
||||
end
|
||||
|
||||
def itself : self
|
||||
self
|
||||
end
|
||||
|
||||
def itself? : self?
|
||||
self.as(self?)
|
||||
end
|
||||
|
||||
def generic(thing : T) : Array(T) forall T
|
||||
Array.new(100) { thing }
|
||||
end
|
||||
|
||||
def union : Int32 | String
|
||||
42.as(Int32 | String)
|
||||
end
|
||||
|
||||
def capture(&block : -> T) forall T
|
||||
block
|
||||
end
|
||||
|
||||
def capture(thing : T, &block : T -> T) forall T
|
||||
block.call(thing)
|
||||
block
|
||||
end
|
||||
|
||||
def range(r : Range)
|
||||
r
|
||||
end
|
||||
end
|
||||
|
||||
mock Test, make_nilable: nil
|
||||
|
||||
let(fake) { mock(Test) }
|
||||
|
||||
it "handles free variables" do
|
||||
allow(fake).to receive(:return_this).and_return("different")
|
||||
expect(fake.return_this("test")).to eq("different")
|
||||
end
|
||||
|
||||
it "raises on type cast error with free variables" do
|
||||
allow(fake).to receive(:return_this).and_return(42)
|
||||
expect { fake.return_this("test") }.to raise_error(TypeCastError, /String/)
|
||||
end
|
||||
|
||||
it "handles free variables with a block" do
|
||||
allow(fake).to receive(:map).and_return("stub")
|
||||
expect(fake.map(:mapped, &.to_s)).to eq("stub")
|
||||
end
|
||||
|
||||
it "raises on type cast error with a block and free variables" do
|
||||
allow(fake).to receive(:map).and_return(42)
|
||||
expect { fake.map(:mapped, &.to_s) }.to raise_error(TypeCastError, /String/)
|
||||
end
|
||||
|
||||
it "handles nilable free variables" do
|
||||
expect(fake.make_nilable("foo")).to be_nil
|
||||
end
|
||||
|
||||
it "handles 'self' return type" do
|
||||
not_self = mock(Test)
|
||||
allow(fake).to receive(:itself).and_return(not_self)
|
||||
expect(fake.itself).to be(not_self)
|
||||
end
|
||||
|
||||
it "raises on type cast error with 'self' return type" do
|
||||
allow(fake).to receive(:itself).and_return(42)
|
||||
expect { fake.itself }.to raise_error(TypeCastError, /#{class_mock(Test)}/)
|
||||
end
|
||||
|
||||
it "handles nilable 'self' return type" do
|
||||
not_self = mock(Test)
|
||||
allow(fake).to receive(:itself?).and_return(not_self)
|
||||
expect(fake.itself?).to be(not_self)
|
||||
end
|
||||
|
||||
it "handles generic return type" do
|
||||
allow(fake).to receive(:generic).and_return([42])
|
||||
expect(fake.generic(42)).to eq([42])
|
||||
end
|
||||
|
||||
it "raises on type cast error with generic return type" do
|
||||
allow(fake).to receive(:generic).and_return("test")
|
||||
expect { fake.generic(42) }.to raise_error(TypeCastError, /Array\(Int32\)/)
|
||||
end
|
||||
|
||||
it "handles union return types" do
|
||||
allow(fake).to receive(:union).and_return("test")
|
||||
expect(fake.union).to eq("test")
|
||||
end
|
||||
|
||||
it "raises on type cast error with union return type" do
|
||||
allow(fake).to receive(:union).and_return(:test)
|
||||
expect { fake.union }.to raise_error(TypeCastError, /Symbol/)
|
||||
end
|
||||
|
||||
it "handles captured blocks" do
|
||||
proc = ->{}
|
||||
allow(fake).to receive(:capture).and_return(proc)
|
||||
expect(fake.capture { nil }).to be(proc)
|
||||
end
|
||||
|
||||
it "raises on type cast error with captured blocks" do
|
||||
proc = ->{ 42 }
|
||||
allow(fake).to receive(:capture).and_return(proc)
|
||||
expect { fake.capture { "other" } }.to raise_error(TypeCastError, /Proc\(String\)/)
|
||||
end
|
||||
|
||||
it "handles captured blocks with arguments" do
|
||||
proc = ->(x : Int32) { x * 2 }
|
||||
allow(fake).to receive(:capture).and_return(proc)
|
||||
expect(fake.capture(5) { 5 }).to be(proc)
|
||||
end
|
||||
|
||||
it "handles range comparisons against non-comparable types" do
|
||||
range = 1..10
|
||||
allow(fake).to receive(:range).and_return(range)
|
||||
expect(fake.range(1..3)).to eq(range)
|
||||
end
|
||||
end
|
6
spec/issues/github_issue_49_spec.cr
Normal file
6
spec/issues/github_issue_49_spec.cr
Normal file
|
@ -0,0 +1,6 @@
|
|||
require "../spec_helper"
|
||||
|
||||
# https://github.com/icy-arctic-fox/spectator/issues/49
|
||||
Spectator.describe "GitHub Issue #49" do
|
||||
# mock File
|
||||
end
|
109
spec/issues/gitlab_issue_51_spec.cr
Normal file
109
spec/issues/gitlab_issue_51_spec.cr
Normal file
|
@ -0,0 +1,109 @@
|
|||
require "../spec_helper"
|
||||
|
||||
module GitLabIssue51
|
||||
class Foo
|
||||
def call(str : String) : String?
|
||||
""
|
||||
end
|
||||
|
||||
def alt1_call(str : String) : String?
|
||||
nil
|
||||
end
|
||||
|
||||
def alt2_call(str : String) : String?
|
||||
[str, nil].sample
|
||||
end
|
||||
end
|
||||
|
||||
class Bar
|
||||
def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call.
|
||||
a_foo.call("")
|
||||
a_foo.alt1_call("")
|
||||
a_foo.alt2_call("")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Spectator.describe GitLabIssue51::Bar do
|
||||
mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: ""
|
||||
|
||||
let(:foo) { mock(GitLabIssue51::Foo) }
|
||||
subject(:call) { described_class.new.call(foo) }
|
||||
|
||||
describe "#call" do
|
||||
it "invokes Foo#call" do
|
||||
call
|
||||
expect(foo).to have_received(:call)
|
||||
end
|
||||
|
||||
it "invokes Foo#alt1_call" do
|
||||
call
|
||||
expect(foo).to have_received(:alt1_call)
|
||||
end
|
||||
|
||||
it "invokes Foo#alt2_call" do
|
||||
call
|
||||
expect(foo).to have_received(:alt2_call)
|
||||
end
|
||||
|
||||
describe "with an explicit return of nil" do
|
||||
it "should invoke Foo#call?" do
|
||||
allow(foo).to receive(:call).and_return(nil)
|
||||
call
|
||||
expect(foo).to have_received(:call)
|
||||
end
|
||||
|
||||
it "invokes Foo#alt1_call" do
|
||||
allow(foo).to receive(:alt1_call).and_return(nil)
|
||||
call
|
||||
expect(foo).to have_received(:alt1_call)
|
||||
end
|
||||
|
||||
it "invokes Foo#alt2_call" do
|
||||
allow(foo).to receive(:alt2_call).and_return(nil)
|
||||
call
|
||||
expect(foo).to have_received(:alt2_call)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with returns set in before_each for all calls" do
|
||||
before_each do
|
||||
allow(foo).to receive(:call).and_return(nil)
|
||||
allow(foo).to receive(:alt1_call).and_return(nil)
|
||||
allow(foo).to receive(:alt2_call).and_return(nil)
|
||||
end
|
||||
|
||||
it "should invoke Foo#call?" do
|
||||
call
|
||||
expect(foo).to have_received(:call)
|
||||
end
|
||||
|
||||
it "should invoke Foo#alt1_call?" do
|
||||
call
|
||||
expect(foo).to have_received(:alt1_call)
|
||||
end
|
||||
|
||||
it "should invoke Foo#alt2_call?" do
|
||||
call
|
||||
expect(foo).to have_received(:alt2_call)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with returns set in before_each for alt calls only" do
|
||||
before_each do
|
||||
allow(foo).to receive(:alt1_call).and_return(nil)
|
||||
allow(foo).to receive(:alt2_call).and_return(nil)
|
||||
end
|
||||
|
||||
it "invokes Foo#alt1_call" do
|
||||
call
|
||||
expect(foo).to have_received(:alt1_call)
|
||||
end
|
||||
|
||||
it "invokes Foo#alt2_call" do
|
||||
call
|
||||
expect(foo).to have_received(:alt2_call)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
6
spec/issues/gitlab_issue_76_spec.cr
Normal file
6
spec/issues/gitlab_issue_76_spec.cr
Normal file
|
@ -0,0 +1,6 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe "GitLab Issue #76" do
|
||||
let(:value) { nil.as(Int32?) }
|
||||
specify { expect(value).to be_nil }
|
||||
end
|
10
spec/issues/gitlab_issue_77_spec.cr
Normal file
10
spec/issues/gitlab_issue_77_spec.cr
Normal file
|
@ -0,0 +1,10 @@
|
|||
require "../spec_helper"
|
||||
|
||||
# https://gitlab.com/arctic-fox/spectator/-/issues/77
|
||||
Spectator.describe "GitLab Issue #77" do
|
||||
it "fails" do
|
||||
expect_raises do
|
||||
raise "Error!"
|
||||
end
|
||||
end
|
||||
end
|
30
spec/issues/gitlab_issue_80_spec.cr
Normal file
30
spec/issues/gitlab_issue_80_spec.cr
Normal file
|
@ -0,0 +1,30 @@
|
|||
require "../spec_helper"
|
||||
|
||||
# https://gitlab.com/arctic-fox/spectator/-/issues/80
|
||||
|
||||
class Item
|
||||
end
|
||||
|
||||
class ItemUser
|
||||
@item = Item.new
|
||||
|
||||
def item
|
||||
@item
|
||||
end
|
||||
end
|
||||
|
||||
Spectator.describe "test1" do
|
||||
it "without mock" do
|
||||
item_user = ItemUser.new
|
||||
item = item_user.item
|
||||
item == item
|
||||
end
|
||||
end
|
||||
|
||||
Spectator.describe "test2" do
|
||||
mock Item do
|
||||
end
|
||||
|
||||
it "without mock" do
|
||||
end
|
||||
end
|
|
@ -1,14 +1,14 @@
|
|||
require "../spec_helper"
|
||||
|
||||
Spectator.describe Spectator::Matchers::ReceiveMatcher do
|
||||
let(stub) { Spectator::NullStub.new(:test_method) }
|
||||
let(stub) { Mocks::NilStub.new(:test_method) }
|
||||
subject(matcher) { described_class.new(stub) }
|
||||
|
||||
let(args) { Spectator::Arguments.capture(1, "test", Symbol, foo: /bar/) }
|
||||
let(args_stub) { Spectator::NullStub.new(:test_method, args) }
|
||||
let(args) { Mocks::ArgumentsPattern.build(1, "test", Symbol, foo: /bar/) }
|
||||
let(args_stub) { Mocks::NilStub.new(:test_method, args) }
|
||||
let(args_matcher) { described_class.new(args_stub) }
|
||||
|
||||
let(no_args_stub) { Spectator::NullStub.new(:test_method, Spectator::Arguments.none) }
|
||||
let(no_args_stub) { Mocks::NilStub.new(:test_method, Mocks::ArgumentsPattern.none) }
|
||||
let(no_args_matcher) { described_class.new(no_args_stub) }
|
||||
|
||||
double(:dbl, test_method: nil, irrelevant: nil)
|
||||
|
@ -169,7 +169,7 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do
|
|||
end
|
||||
|
||||
context "with method calls" do
|
||||
before_each do
|
||||
before do
|
||||
dbl.test_method
|
||||
dbl.test_method(1, "wrong", :xyz, foo: "foobarbaz")
|
||||
dbl.irrelevant("foo")
|
||||
|
@ -289,7 +289,7 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do
|
|||
|
||||
pre_condition { expect(match_data).to be_a(failed_match) }
|
||||
|
||||
before_each do
|
||||
before do
|
||||
dbl.test_method
|
||||
dbl.test_method(1, "test", :xyz, foo: "foobarbaz")
|
||||
dbl.irrelevant("foo")
|
||||
|
|
|
@ -52,7 +52,7 @@ Spectator.describe "Explicit Subject" do
|
|||
describe Array(Int32) do # TODO: Multiple arguments to describe/context.
|
||||
subject { [] of Int32 }
|
||||
|
||||
before_each { subject.push(1, 2, 3) }
|
||||
before { subject.push(1, 2, 3) }
|
||||
|
||||
it "has the prescribed elements" do
|
||||
expect(subject).to eq([1, 2, 3])
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
require "./spec_helper"
|
||||
|
||||
# This is a meta test that ensures specs can be compiled and run at runtime.
|
||||
# The purpose of this is to report an error if this process fails.
|
||||
# Other tests will fail, but display a different name/description of the test.
|
||||
# This clearly indicates that runtime testing failed.
|
||||
#
|
||||
# Runtime compilation is used to get output of tests as well as check syntax.
|
||||
# Some specs are too complex to be ran normally.
|
||||
# Additionally, this allows examples to easily check specific failure cases.
|
||||
# Plus, it makes testing user-reported issues easy.
|
||||
Spectator.describe "Runtime compilation", :slow, :compile do
|
||||
given_example passing_example do
|
||||
it "does something" do
|
||||
expect(true).to be_true
|
||||
end
|
||||
end
|
||||
|
||||
it "can compile and retrieve the result of an example" do
|
||||
expect(passing_example).to be_successful
|
||||
end
|
||||
|
||||
it "can retrieve expectations" do
|
||||
expect(passing_example.expectations).to_not be_empty
|
||||
end
|
||||
|
||||
given_example failing_example do
|
||||
it "does something" do
|
||||
expect(true).to be_false
|
||||
end
|
||||
|
||||
it "doesn't run" do
|
||||
expect(true).to be_false
|
||||
end
|
||||
end
|
||||
|
||||
it "detects failed examples" do
|
||||
expect(failing_example).to be_failure
|
||||
end
|
||||
|
||||
given_example malformed_example do
|
||||
it "does something" do
|
||||
asdf
|
||||
end
|
||||
end
|
||||
|
||||
it "raises on compilation errors" do
|
||||
expect { malformed_example }.to raise_error(/compilation/i)
|
||||
end
|
||||
|
||||
given_expectation satisfied_expectation do
|
||||
expect(true).to be_true
|
||||
end
|
||||
|
||||
it "can compile and retrieve expectations" do
|
||||
expect(satisfied_expectation).to be_satisfied
|
||||
end
|
||||
end
|
|
@ -15,35 +15,3 @@ end
|
|||
macro specify_fails(description = nil, &block)
|
||||
it_fails {{description}} {{block}}
|
||||
end
|
||||
|
||||
# Defines an example ("it" block) that is lazily compiled.
|
||||
# When the example is referenced with *id*, it will be compiled and the results retrieved.
|
||||
# The value returned by *id* will be a `Spectator::SpecHelpers::Result`.
|
||||
# This allows the test result to be inspected.
|
||||
macro given_example(id, &block)
|
||||
let({{id}}) do
|
||||
::Spectator::SpecHelpers::Example.new(
|
||||
{{__FILE__}},
|
||||
{{id.id.stringify}},
|
||||
{{block.body.stringify}}
|
||||
).result
|
||||
end
|
||||
end
|
||||
|
||||
# Defines an example ("it" block) that is lazily compiled.
|
||||
# The "it" block must be omitted, as the block provided to this macro will be wrapped in one.
|
||||
# When the expectation is referenced with *id*, it will be compiled and the result retrieved.
|
||||
# The value returned by *id* will be a `Spectator::SpecHelpers::Expectation`.
|
||||
# This allows an expectation to be inspected.
|
||||
# Only the last expectation performed will be returned.
|
||||
# An error is raised if no expectations ran.
|
||||
macro given_expectation(id, &block)
|
||||
let({{id}}) do
|
||||
result = ::Spectator::SpecHelpers::Example.new(
|
||||
{{__FILE__}},
|
||||
{{id.id.stringify}},
|
||||
{{"it do\n" + block.body.stringify + "\nend"}}
|
||||
).result
|
||||
result.expectations.last || raise("No expectations found from {{id.id}}")
|
||||
end
|
||||
end
|
||||
|
|
188
spec/spectator/dsl/mocks/allow_receive_spec.cr
Normal file
188
spec/spectator/dsl/mocks/allow_receive_spec.cr
Normal file
|
@ -0,0 +1,188 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
Spectator.describe "Allow stub DSL" do
|
||||
context "with a double" do
|
||||
double(:dbl) do
|
||||
# Ensure the original is never called.
|
||||
stub abstract def foo : Nil
|
||||
stub abstract def foo(arg) : Nil
|
||||
stub abstract def value : Int32
|
||||
end
|
||||
|
||||
let(dbl) { double(:dbl) }
|
||||
|
||||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
allow(dbl).to receive(:foo)
|
||||
expect { dbl.foo }.to_not raise_error
|
||||
end
|
||||
|
||||
it "returns the correct value" do
|
||||
allow(dbl).to receive(:value).and_return(42)
|
||||
expect(dbl.value).to eq(42)
|
||||
end
|
||||
|
||||
it "matches when a message is received with matching arguments" do
|
||||
allow(dbl).to receive(:foo).with(:bar)
|
||||
expect { dbl.foo(:bar) }.to_not raise_error
|
||||
end
|
||||
|
||||
it "raises when a message without arguments is received" do
|
||||
allow(dbl).to receive(:foo).with(:bar)
|
||||
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/)
|
||||
end
|
||||
|
||||
it "raises when a message with different arguments is received" do
|
||||
allow(dbl).to receive(:foo).with(:baz)
|
||||
expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a class double" do
|
||||
double(:dbl) do
|
||||
# Ensure the original is never called.
|
||||
stub abstract def self.foo : Nil
|
||||
end
|
||||
|
||||
stub abstract def self.foo(arg) : Nil
|
||||
end
|
||||
|
||||
stub abstract def self.value : Int32
|
||||
42
|
||||
end
|
||||
end
|
||||
|
||||
let(dbl) { class_double(:dbl) }
|
||||
|
||||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
allow(dbl).to receive(:foo)
|
||||
expect { dbl.foo }.to_not raise_error
|
||||
end
|
||||
|
||||
it "returns the correct value" do
|
||||
allow(dbl).to receive(:value).and_return(42)
|
||||
expect(dbl.value).to eq(42)
|
||||
end
|
||||
|
||||
it "matches when a message is received with matching arguments" do
|
||||
allow(dbl).to receive(:foo).with(:bar)
|
||||
expect { dbl.foo(:bar) }.to_not raise_error
|
||||
end
|
||||
|
||||
it "raises when a message without arguments is received" do
|
||||
allow(dbl).to receive(:foo).with(:bar)
|
||||
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/)
|
||||
end
|
||||
|
||||
it "raises when a message with different arguments is received" do
|
||||
allow(dbl).to receive(:foo).with(:baz)
|
||||
expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a mock" do
|
||||
abstract class MyClass
|
||||
abstract def foo : Int32
|
||||
abstract def foo(arg) : Int32
|
||||
end
|
||||
|
||||
mock(MyClass)
|
||||
|
||||
let(fake) { mock(MyClass) }
|
||||
|
||||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage)
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
allow(fake).to receive(:foo).and_return(42)
|
||||
expect(fake.foo).to eq(42)
|
||||
end
|
||||
|
||||
it "returns the correct value" do
|
||||
allow(fake).to receive(:foo).and_return(42)
|
||||
expect(fake.foo).to eq(42)
|
||||
end
|
||||
|
||||
it "matches when a message is received with matching arguments" do
|
||||
allow(fake).to receive(:foo).with(:bar).and_return(42)
|
||||
expect(fake.foo(:bar)).to eq(42)
|
||||
end
|
||||
|
||||
it "raises when a message without arguments is received" do
|
||||
allow(fake).to receive(:foo).with(:bar)
|
||||
expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/)
|
||||
end
|
||||
|
||||
it "raises when a message with different arguments is received" do
|
||||
allow(fake).to receive(:foo).with(:baz)
|
||||
expect { fake.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a class mock" do
|
||||
class MyClass
|
||||
def self.foo : Int32
|
||||
42
|
||||
end
|
||||
|
||||
def self.foo(arg) : Int32
|
||||
42
|
||||
end
|
||||
end
|
||||
|
||||
mock(MyClass)
|
||||
|
||||
let(fake) { class_mock(MyClass) }
|
||||
|
||||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition { expect(fake.foo).to eq(42) }
|
||||
|
||||
it "matches when a message is received" do
|
||||
allow(fake).to receive(:foo).and_return(0)
|
||||
expect(fake.foo).to eq(0)
|
||||
end
|
||||
|
||||
it "returns the correct value" do
|
||||
allow(fake).to receive(:foo).and_return(0)
|
||||
expect(fake.foo).to eq(0)
|
||||
end
|
||||
|
||||
it "matches when a message is received with matching arguments" do
|
||||
allow(fake).to receive(:foo).with(:bar).and_return(0)
|
||||
expect(fake.foo(:bar)).to eq(0)
|
||||
end
|
||||
|
||||
it "calls the original when a message without arguments is received" do
|
||||
allow(fake).to receive(:foo).with(:bar)
|
||||
expect(fake.foo).to eq(42)
|
||||
end
|
||||
|
||||
it "calls the original when a message with different arguments is received" do
|
||||
allow(fake).to receive(:foo).with(:baz)
|
||||
expect(fake.foo(:bar)).to eq(42)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -168,7 +168,7 @@ Spectator.describe "Double DSL", :smoke do
|
|||
|
||||
context "methods accepting blocks" do
|
||||
double(:test7) do
|
||||
stub def foo
|
||||
stub def foo(&)
|
||||
yield
|
||||
end
|
||||
|
||||
|
@ -312,7 +312,7 @@ Spectator.describe "Double DSL", :smoke do
|
|||
let(override) { :override }
|
||||
let(dbl) { double(:context_double, override: override) }
|
||||
|
||||
before_each { allow(dbl).to receive(:memoize).and_return(memoize) }
|
||||
before { allow(dbl).to receive(:memoize).and_return(memoize) }
|
||||
|
||||
it "doesn't change predefined values" do
|
||||
expect(dbl.predefined).to eq(:predefined)
|
||||
|
@ -337,7 +337,7 @@ Spectator.describe "Double DSL", :smoke do
|
|||
|
||||
describe "class doubles" do
|
||||
double(:class_double) do
|
||||
abstract_stub def self.abstract_method
|
||||
stub abstract def self.abstract_method
|
||||
:abstract
|
||||
end
|
||||
|
||||
|
|
|
@ -14,6 +14,12 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
|
||||
dbl._spectator_clear_calls # Don't include previous call in results.
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
expect(dbl).to receive(:foo)
|
||||
dbl.foo
|
||||
|
@ -51,13 +57,13 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
context "with a class double" do
|
||||
double(:dbl) do
|
||||
# Ensure the original is never called.
|
||||
abstract_stub def self.foo : Nil
|
||||
stub abstract def self.foo : Nil
|
||||
end
|
||||
|
||||
abstract_stub def self.foo(arg) : Nil
|
||||
stub abstract def self.foo(arg) : Nil
|
||||
end
|
||||
|
||||
abstract_stub def self.value : Int32
|
||||
stub abstract def self.value : Int32
|
||||
42
|
||||
end
|
||||
end
|
||||
|
@ -67,6 +73,12 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
|
||||
dbl._spectator_clear_calls # Don't include previous call in results.
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
expect(dbl).to receive(:foo)
|
||||
dbl.foo
|
||||
|
@ -114,6 +126,12 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage)
|
||||
fake._spectator_clear_calls # Don't include previous call in results.
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
expect(fake).to receive(:foo).and_return(42)
|
||||
fake.foo(:bar)
|
||||
|
@ -166,14 +184,20 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
# Ensure invocations don't leak between examples.
|
||||
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
|
||||
|
||||
# Ensure stubs don't leak between examples.
|
||||
pre_condition do
|
||||
expect(fake.foo).to eq(42)
|
||||
fake._spectator_clear_calls # Don't include previous call in results.
|
||||
end
|
||||
|
||||
it "matches when a message is received" do
|
||||
expect(fake).to receive(:foo).and_return(42)
|
||||
expect(fake).to receive(:foo).and_return(0)
|
||||
fake.foo(:bar)
|
||||
end
|
||||
|
||||
it "returns the correct value" do
|
||||
expect(fake).to receive(:foo).and_return(42)
|
||||
expect(fake.foo).to eq(42)
|
||||
expect(fake).to receive(:foo).and_return(0)
|
||||
expect(fake.foo).to eq(0)
|
||||
end
|
||||
|
||||
it "matches when a message isn't received" do
|
||||
|
@ -181,12 +205,12 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
end
|
||||
|
||||
it "matches when a message is received with matching arguments" do
|
||||
expect(fake).to receive(:foo).with(:bar).and_return(42)
|
||||
expect(fake).to receive(:foo).with(:bar).and_return(0)
|
||||
fake.foo(:bar)
|
||||
end
|
||||
|
||||
it "matches when a message without arguments is received" do
|
||||
expect(fake).to_not receive(:foo).with(:bar).and_return(42)
|
||||
expect(fake).to_not receive(:foo).with(:bar).and_return(0)
|
||||
fake.foo
|
||||
end
|
||||
|
||||
|
@ -195,7 +219,7 @@ Spectator.describe "Deferred stub expectation DSL" do
|
|||
end
|
||||
|
||||
it "matches when a message with arguments isn't received" do
|
||||
expect(fake).to_not receive(:foo).with(:baz).and_return(42)
|
||||
expect(fake).to_not receive(:foo).with(:baz).and_return(0)
|
||||
fake.foo(:bar)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
args[1].as(Int32),
|
||||
args[2].as(Int32),
|
||||
},
|
||||
args[3].as(Int32),
|
||||
args[:kwarg].as(Int32),
|
||||
{
|
||||
x: args[:x].as(Int32),
|
||||
y: args[:y].as(Int32),
|
||||
|
@ -40,17 +40,17 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
arg
|
||||
end
|
||||
|
||||
def method4 : Symbol
|
||||
def method4(&) : Symbol
|
||||
@_spectator_invocations << :method4
|
||||
yield
|
||||
end
|
||||
|
||||
def method5
|
||||
def method5(&)
|
||||
@_spectator_invocations << :method5
|
||||
yield.to_i
|
||||
end
|
||||
|
||||
def method6
|
||||
def method6(&)
|
||||
@_spectator_invocations << :method6
|
||||
yield
|
||||
end
|
||||
|
@ -60,7 +60,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
{arg, args, kwarg, kwargs}
|
||||
end
|
||||
|
||||
def method8(arg, *args, kwarg, **kwargs)
|
||||
def method8(arg, *args, kwarg, **kwargs, &)
|
||||
@_spectator_invocations << :method8
|
||||
yield
|
||||
{arg, args, kwarg, kwargs}
|
||||
|
@ -80,7 +80,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
"stubbed"
|
||||
end
|
||||
|
||||
stub def method4 : Symbol
|
||||
stub def method4(&) : Symbol
|
||||
yield
|
||||
:block
|
||||
end
|
||||
|
@ -253,25 +253,25 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
|
||||
stub def method5
|
||||
stub def method5(&)
|
||||
yield
|
||||
end
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
stub def method6 : Symbol
|
||||
stub def method6(&) : Symbol
|
||||
yield
|
||||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
abstract_stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
end
|
||||
|
||||
subject(fake) { mock(AbstractClass) }
|
||||
|
@ -373,20 +373,20 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
mock(AbstractClass) do
|
||||
# NOTE: Abstract methods without a type restriction on the return value
|
||||
# must be implemented with a type restriction.
|
||||
abstract_stub abstract def method1 : String
|
||||
stub abstract def method1 : String
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
|
||||
stub def method5
|
||||
stub def method5(&)
|
||||
yield
|
||||
end
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
stub def method6 : Symbol
|
||||
stub def method6(&) : Symbol
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
@ -449,25 +449,25 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
|
||||
stub def method5
|
||||
stub def method5(&)
|
||||
yield
|
||||
end
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
stub def method6 : Symbol
|
||||
stub def method6(&) : Symbol
|
||||
yield
|
||||
end
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
stub abstract def method7(arg, *args, kwarg, **kwargs) : CapturedArguments
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
abstract_stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
stub abstract def method8(arg, *args, kwarg, **kwargs, &) : CapturedArguments
|
||||
end
|
||||
|
||||
subject(fake) { mock(AbstractStruct) }
|
||||
|
@ -569,20 +569,20 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
mock(AbstractStruct) do
|
||||
# NOTE: Abstract methods without a type restriction on the return value
|
||||
# must be implemented with a type restriction.
|
||||
abstract_stub abstract def method1 : String
|
||||
stub abstract def method1 : String
|
||||
|
||||
# NOTE: Defining the stub here with a return type restriction, but no default implementation.
|
||||
abstract_stub abstract def method4 : Symbol
|
||||
stub abstract def method4 : Symbol
|
||||
|
||||
# NOTE: Abstract methods that yield must have yield functionality defined in the method.
|
||||
# This requires that yielding methods have a default implementation.
|
||||
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
|
||||
stub def method5
|
||||
stub def method5(&)
|
||||
yield
|
||||
end
|
||||
|
||||
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
|
||||
stub def method6 : Symbol
|
||||
stub def method6(&) : Symbol
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
@ -620,11 +620,11 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
:original
|
||||
end
|
||||
|
||||
def method3
|
||||
def method3(&)
|
||||
yield
|
||||
end
|
||||
|
||||
def method4 : Int32
|
||||
def method4(&) : Int32
|
||||
yield.to_i
|
||||
end
|
||||
|
||||
|
@ -749,11 +749,11 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
:original
|
||||
end
|
||||
|
||||
def method3
|
||||
def method3(&)
|
||||
yield
|
||||
end
|
||||
|
||||
def method4 : Int32
|
||||
def method4(&) : Int32
|
||||
yield.to_i
|
||||
end
|
||||
|
||||
|
@ -947,7 +947,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
let(override) { :override }
|
||||
let(fake) { mock(Dummy, override: override) }
|
||||
|
||||
before_each { allow(fake).to receive(:memoize).and_return(memoize) }
|
||||
before { allow(fake).to receive(:memoize).and_return(memoize) }
|
||||
|
||||
it "doesn't change predefined values" do
|
||||
expect(fake.predefined).to eq(:predefined)
|
||||
|
@ -994,7 +994,7 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
end
|
||||
|
||||
mock(Dummy) do
|
||||
abstract_stub def self.abstract_method
|
||||
stub abstract def self.abstract_method
|
||||
:abstract
|
||||
end
|
||||
|
||||
|
@ -1027,4 +1027,262 @@ Spectator.describe "Mock DSL", :smoke do
|
|||
expect(fake.reference).to eq("reference")
|
||||
end
|
||||
end
|
||||
|
||||
describe "mock module" do
|
||||
module Dummy
|
||||
# `extend self` cannot be used.
|
||||
# The Crystal compiler doesn't report the methods as class methods when doing so.
|
||||
|
||||
def self.abstract_method
|
||||
:not_really_abstract
|
||||
end
|
||||
|
||||
def self.default_method
|
||||
:original
|
||||
end
|
||||
|
||||
def self.args(arg)
|
||||
arg
|
||||
end
|
||||
|
||||
def self.method1
|
||||
:original
|
||||
end
|
||||
|
||||
def self.reference
|
||||
method1.to_s
|
||||
end
|
||||
end
|
||||
|
||||
mock(Dummy) do
|
||||
stub abstract def self.abstract_method
|
||||
:abstract
|
||||
end
|
||||
|
||||
stub def self.default_method
|
||||
:default
|
||||
end
|
||||
end
|
||||
|
||||
let(fake) { class_mock(Dummy) }
|
||||
|
||||
it "raises on abstract stubs" do
|
||||
expect { fake.abstract_method }.to raise_error(Spectator::UnexpectedMessage, /abstract_method/)
|
||||
end
|
||||
|
||||
it "can define default stubs" do
|
||||
expect(fake.default_method).to eq(:default)
|
||||
end
|
||||
|
||||
it "can define new stubs" do
|
||||
expect { allow(fake).to receive(:args).and_return(42) }.to change { fake.args(5) }.from(5).to(42)
|
||||
end
|
||||
|
||||
it "can override class method stubs" do
|
||||
allow(fake).to receive(:method1).and_return(:override)
|
||||
expect(fake.method1).to eq(:override)
|
||||
end
|
||||
|
||||
xit "can reference stubs", pending: "Default stub of module class methods always refer to original" do
|
||||
allow(fake).to receive(:method1).and_return(:reference)
|
||||
expect(fake.reference).to eq("reference")
|
||||
end
|
||||
end
|
||||
|
||||
context "with a class including a mocked module" do
|
||||
module Dummy
|
||||
getter _spectator_invocations = [] of Symbol
|
||||
|
||||
def method1
|
||||
@_spectator_invocations << :method1
|
||||
"original"
|
||||
end
|
||||
|
||||
def method2 : Symbol
|
||||
@_spectator_invocations << :method2
|
||||
:original
|
||||
end
|
||||
|
||||
def method3(arg)
|
||||
@_spectator_invocations << :method3
|
||||
arg
|
||||
end
|
||||
|
||||
def method4(&) : Symbol
|
||||
@_spectator_invocations << :method4
|
||||
yield
|
||||
end
|
||||
|
||||
def method5(&)
|
||||
@_spectator_invocations << :method5
|
||||
yield.to_i
|
||||
end
|
||||
|
||||
def method6(&)
|
||||
@_spectator_invocations << :method6
|
||||
yield
|
||||
end
|
||||
|
||||
def method7(arg, *args, kwarg, **kwargs)
|
||||
@_spectator_invocations << :method7
|
||||
{arg, args, kwarg, kwargs}
|
||||
end
|
||||
|
||||
def method8(arg, *args, kwarg, **kwargs, &)
|
||||
@_spectator_invocations << :method8
|
||||
yield
|
||||
{arg, args, kwarg, kwargs}
|
||||
end
|
||||
end
|
||||
|
||||
# method1 stubbed via mock block
|
||||
# method2 stubbed via keyword args
|
||||
# method3 not stubbed (calls original)
|
||||
# method4 stubbed via mock block (yields)
|
||||
# method5 stubbed via keyword args (yields)
|
||||
# method6 not stubbed (calls original and yields)
|
||||
# method7 not stubbed (calls original) testing args
|
||||
# method8 not stubbed (calls original and yields) testing args
|
||||
mock(Dummy, method2: :stubbed, method5: 42) do
|
||||
stub def method1
|
||||
"stubbed"
|
||||
end
|
||||
|
||||
stub def method4(&) : Symbol
|
||||
yield
|
||||
:block
|
||||
end
|
||||
end
|
||||
|
||||
subject(fake) { mock(Dummy) }
|
||||
|
||||
it "defines a subclass" do
|
||||
expect(fake).to be_a(Dummy)
|
||||
end
|
||||
|
||||
it "defines stubs in the block" do
|
||||
expect(fake.method1).to eq("stubbed")
|
||||
end
|
||||
|
||||
it "can stub methods defined in the block" do
|
||||
stub = Spectator::ValueStub.new(:method1, "override")
|
||||
expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override")
|
||||
end
|
||||
|
||||
it "defines stubs from keyword arguments" do
|
||||
expect(fake.method2).to eq(:stubbed)
|
||||
end
|
||||
|
||||
it "can stub methods from keyword arguments" do
|
||||
stub = Spectator::ValueStub.new(:method2, :override)
|
||||
expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override)
|
||||
end
|
||||
|
||||
it "calls the original implementation for methods not provided a stub" do
|
||||
expect(fake.method3(:xyz)).to eq(:xyz)
|
||||
end
|
||||
|
||||
it "can stub methods after declaration" do
|
||||
stub = Spectator::ValueStub.new(:method3, :abc)
|
||||
expect { fake._spectator_define_stub(stub) }.to change { fake.method3(:xyz) }.from(:xyz).to(:abc)
|
||||
end
|
||||
|
||||
it "defines stubs with yield in the block" do
|
||||
expect(fake.method4 { :wrong }).to eq(:block)
|
||||
end
|
||||
|
||||
it "can stub methods with yield in the block" do
|
||||
stub = Spectator::ValueStub.new(:method4, :override)
|
||||
expect { fake._spectator_define_stub(stub) }.to change { fake.method4 { :wrong } }.from(:block).to(:override)
|
||||
end
|
||||
|
||||
it "defines stubs with yield from keyword arguments" do
|
||||
expect(fake.method5 { :wrong }).to eq(42)
|
||||
end
|
||||
|
||||
it "can stub methods with yield from keyword arguments" do
|
||||
stub = Spectator::ValueStub.new(:method5, 123)
|
||||
expect { fake._spectator_define_stub(stub) }.to change { fake.method5 { "0" } }.from(42).to(123)
|
||||
end
|
||||
|
||||
it "can stub yielding methods after declaration" do
|
||||
stub = Spectator::ValueStub.new(:method6, :abc)
|
||||
expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :xyz } }.from(:xyz).to(:abc)
|
||||
end
|
||||
|
||||
it "handles arguments correctly" do
|
||||
args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
|
||||
args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
|
||||
aggregate_failures do
|
||||
expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||
expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||
end
|
||||
end
|
||||
|
||||
it "handles arguments correctly with stubs" do
|
||||
stub1 = Spectator::ProcStub.new(:method7, args_proc)
|
||||
stub2 = Spectator::ProcStub.new(:method8, args_proc)
|
||||
fake._spectator_define_stub(stub1)
|
||||
fake._spectator_define_stub(stub2)
|
||||
args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
|
||||
args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
|
||||
aggregate_failures do
|
||||
expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||
expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
|
||||
end
|
||||
end
|
||||
|
||||
it "compiles types without unions" do
|
||||
aggregate_failures do
|
||||
expect(fake.method1).to compile_as(String)
|
||||
expect(fake.method2).to compile_as(Symbol)
|
||||
expect(fake.method3(42)).to compile_as(Int32)
|
||||
expect(fake.method4 { :foo }).to compile_as(Symbol)
|
||||
expect(fake.method5 { "123" }).to compile_as(Int32)
|
||||
expect(fake.method6 { "123" }).to compile_as(String)
|
||||
end
|
||||
end
|
||||
|
||||
def restricted(thing : Dummy)
|
||||
thing.method1
|
||||
end
|
||||
|
||||
it "can be used in type restricted methods" do
|
||||
expect(restricted(fake)).to eq("stubbed")
|
||||
end
|
||||
|
||||
it "does not call the original method when stubbed" do
|
||||
fake.method1
|
||||
fake.method2
|
||||
fake.method3("foo")
|
||||
fake.method4 { :foo }
|
||||
fake.method5 { "42" }
|
||||
fake.method6 { 42 }
|
||||
fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
|
||||
fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
|
||||
expect(fake._spectator_invocations).to contain_exactly(:method3, :method6, :method7, :method8)
|
||||
end
|
||||
|
||||
# Cannot test unexpected messages - will not compile due to missing methods.
|
||||
|
||||
describe "deferred default stubs" do
|
||||
mock(Dummy)
|
||||
|
||||
let(fake2) do
|
||||
mock(Dummy,
|
||||
method1: "stubbed",
|
||||
method3: 123,
|
||||
method4: :xyz)
|
||||
end
|
||||
|
||||
it "uses the keyword arguments as stubs" do
|
||||
aggregate_failures do
|
||||
expect(fake2.method1).to eq("stubbed")
|
||||
expect(fake2.method2).to eq(:original)
|
||||
expect(fake2.method3(42)).to eq(123)
|
||||
expect(fake2.method4 { :foo }).to eq(:xyz)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -156,7 +156,7 @@ Spectator.describe "Null double DSL" do
|
|||
|
||||
context "methods accepting blocks" do
|
||||
double(:test7) do
|
||||
stub def foo
|
||||
stub def foo(&)
|
||||
yield
|
||||
end
|
||||
|
||||
|
|
|
@ -9,5 +9,31 @@ Spectator.describe Spectator::Allow do
|
|||
it "applies a stub" do
|
||||
expect { alw.to(stub) }.to change { dbl.foo }.from(42).to(123)
|
||||
end
|
||||
|
||||
context "leak" do
|
||||
class Thing
|
||||
def foo
|
||||
42
|
||||
end
|
||||
end
|
||||
|
||||
mock Thing
|
||||
|
||||
getter(thing : Thing) { mock(Thing) }
|
||||
|
||||
# Workaround type restrictions requiring a constant.
|
||||
def fake
|
||||
class_mock(Thing).cast(thing)
|
||||
end
|
||||
|
||||
specify do
|
||||
expect { allow(fake).to(stub) }.to change { fake.foo }.from(42).to(123)
|
||||
end
|
||||
|
||||
# This example must be run after the previous (random order may break this).
|
||||
it "clears the stub after the example completes" do
|
||||
expect { fake.foo }.to eq(42)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
require "../../spec_helper"
|
||||
|
||||
Spectator.describe Spectator::Arguments do
|
||||
subject(arguments) do
|
||||
Spectator::Arguments.new(
|
||||
subject(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "stores the arguments" do
|
||||
expect(arguments).to have_attributes(
|
||||
args: {42, "foo"},
|
||||
kwargs: {bar: "baz", qux: 123}
|
||||
)
|
||||
end
|
||||
|
||||
it "stores the arguments" do
|
||||
expect(arguments.args).to eq({42, "foo"})
|
||||
end
|
||||
|
||||
it "stores the keyword arguments" do
|
||||
expect(arguments.kwargs).to eq({bar: "baz", qux: 123})
|
||||
end
|
||||
|
||||
describe ".capture" do
|
||||
subject { Spectator::Arguments.capture(42, "foo", bar: "baz", qux: 123) }
|
||||
|
||||
|
@ -24,22 +18,20 @@ Spectator.describe Spectator::Arguments do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#[]" do
|
||||
context "with an index" do
|
||||
it "returns a positional argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[0]).to eq(42)
|
||||
expect(arguments[1]).to eq("foo")
|
||||
end
|
||||
describe "#[](index)" do
|
||||
it "returns a positional argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[0]).to eq(42)
|
||||
expect(arguments[1]).to eq("foo")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a symbol" do
|
||||
it "returns a named argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[:bar]).to eq("baz")
|
||||
expect(arguments[:qux]).to eq(123)
|
||||
end
|
||||
describe "#[](symbol)" do
|
||||
it "returns a keyword argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[:bar]).to eq("baz")
|
||||
expect(arguments[:qux]).to eq(123)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -63,50 +55,79 @@ Spectator.describe Spectator::Arguments do
|
|||
describe "#==" do
|
||||
subject { arguments == other }
|
||||
|
||||
context "with equal arguments" do
|
||||
let(other) { arguments }
|
||||
context "with Arguments" do
|
||||
context "with equal arguments" do
|
||||
let(other) { arguments }
|
||||
|
||||
it "returns true" do
|
||||
is_expected.to be_true
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(other) { Spectator::Arguments.new(arguments.args, {qux: 123, bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with an extra kwarg" do
|
||||
let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz", qux: 123, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(other) do
|
||||
Spectator::Arguments.new(
|
||||
args: {123, :foo, "bar"},
|
||||
kwargs: {opt: "foobar"}
|
||||
)
|
||||
context "with FormalArguments" do
|
||||
context "with equal arguments" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
is_expected.to be_false
|
||||
end
|
||||
end
|
||||
context "with different arguments" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(other) do
|
||||
Spectator::Arguments.new(
|
||||
args: arguments.args,
|
||||
kwargs: {qux: 123, bar: "baz"}
|
||||
)
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
is_expected.to be_true
|
||||
end
|
||||
end
|
||||
context "with the same kwargs in a different order" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {qux: 123, bar: "baz"}) }
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(other) do
|
||||
Spectator::Arguments.new(
|
||||
args: arguments.args,
|
||||
kwargs: {bar: "baz"}
|
||||
)
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
is_expected.to be_false
|
||||
context "with a missing kwarg" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with an extra kwarg" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different splat arguments" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with mixed positional tuple types" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42}, :splat, {"foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -114,76 +135,149 @@ Spectator.describe Spectator::Arguments do
|
|||
describe "#===" do
|
||||
subject { pattern === arguments }
|
||||
|
||||
context "with equal arguments" do
|
||||
let(pattern) { arguments }
|
||||
context "with Arguments" do
|
||||
context "with equal arguments" do
|
||||
let(pattern) { arguments }
|
||||
|
||||
it "returns true" do
|
||||
is_expected.to be_true
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with matching arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(pattern) { Spectator::Arguments.new(arguments.args, {qux: Int32, bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with an additional kwarg" do
|
||||
let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/, qux: Int32, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(pattern) do
|
||||
Spectator::Arguments.new(
|
||||
args: {123, :foo, "bar"},
|
||||
kwargs: {opt: "foobar"}
|
||||
)
|
||||
context "with FormalArguments" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
context "with equal arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
is_expected.to be_false
|
||||
end
|
||||
end
|
||||
context "with matching arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) }
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(pattern) do
|
||||
Spectator::Arguments.new(
|
||||
args: arguments.args,
|
||||
kwargs: {qux: 123, bar: "baz"}
|
||||
)
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
is_expected.to be_true
|
||||
end
|
||||
end
|
||||
context "with non-matching arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) }
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(pattern) do
|
||||
Spectator::Arguments.new(
|
||||
args: arguments.args,
|
||||
kwargs: {bar: "baz"}
|
||||
)
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
is_expected.to be_false
|
||||
end
|
||||
end
|
||||
context "with different arguments" do
|
||||
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
|
||||
|
||||
context "with matching types and regex" do
|
||||
let(pattern) do
|
||||
Spectator::Arguments.new(
|
||||
args: {Int32, /foo/},
|
||||
kwargs: {bar: String, qux: 123}
|
||||
)
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
is_expected.to be_true
|
||||
end
|
||||
end
|
||||
context "with the same kwargs in a different order" do
|
||||
let(pattern) { Spectator::Arguments.new(arguments.positional, {qux: Int32, bar: /baz/}) }
|
||||
|
||||
context "with different types and regex" do
|
||||
let(pattern) do
|
||||
Spectator::Arguments.new(
|
||||
args: {Symbol, /bar/},
|
||||
kwargs: {bar: String, qux: 42}
|
||||
)
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
is_expected.to be_false
|
||||
context "with an additional kwarg" do
|
||||
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/, qux: Int32, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different splat arguments" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
|
||||
let(pattern) { Spectator::Arguments.new({Int32, /foo/, 5}, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with matching mixed positional tuple types" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
|
||||
let(pattern) { Spectator::Arguments.new({Int32, /foo/, 1, 2, 3}, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching mixed positional tuple types" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
|
||||
let(pattern) { Spectator::Arguments.new({Float64, /bar/, 3, 2, Symbol}, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with matching args spilling over into splat and mixed positional tuple types" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
let(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching args spilling over into splat and mixed positional tuple types" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
let(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with matching mixed named positional and keyword arguments" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
let(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching mixed named positional and keyword arguments" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
let(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with non-matching mixed named positional and keyword arguments" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
let(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "with abstract stubs and return type annotations" do
|
||||
Spectator::Double.define(TestDouble) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
stub abstract def foo(value) : String
|
||||
end
|
||||
|
||||
let(arguments) { Spectator::Arguments.capture(/foo/) }
|
||||
|
@ -98,8 +98,8 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "with nillable return type annotations" do
|
||||
Spectator::Double.define(TestDouble) do
|
||||
abstract_stub abstract def foo : String?
|
||||
abstract_stub abstract def bar : Nil
|
||||
stub abstract def foo : String?
|
||||
stub abstract def bar : Nil
|
||||
end
|
||||
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, nil).as(Spectator::Stub) }
|
||||
|
@ -116,7 +116,7 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "with a method that uses NoReturn" do
|
||||
Spectator::Double.define(NoReturnDouble) do
|
||||
abstract_stub abstract def oops : NoReturn
|
||||
stub abstract def oops : NoReturn
|
||||
end
|
||||
|
||||
subject(dbl) { NoReturnDouble.new }
|
||||
|
@ -212,14 +212,10 @@ Spectator.describe Spectator::Double do
|
|||
expect(dbl.hash).to be_a(UInt64)
|
||||
expect(dbl.in?([42])).to be_false
|
||||
expect(dbl.in?(1, 2, 3)).to be_false
|
||||
expect(dbl.inspect).to contain("EmptyDouble")
|
||||
expect(dbl.itself).to be(dbl)
|
||||
expect(dbl.not_nil!).to be(dbl)
|
||||
expect(dbl.pretty_inspect).to contain("EmptyDouble")
|
||||
expect(dbl.pretty_print(pp)).to be_nil
|
||||
expect(dbl.tap { nil }).to be(dbl)
|
||||
expect(dbl.to_s).to contain("EmptyDouble")
|
||||
expect(dbl.to_s(io)).to be_nil
|
||||
expect(dbl.try { nil }).to be_nil
|
||||
expect(dbl.object_id).to be_a(UInt64)
|
||||
expect(dbl.same?(dbl)).to be_true
|
||||
|
@ -237,8 +233,8 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
context "without common object methods" do
|
||||
Spectator::Double.define(TestDouble) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
abstract_stub abstract def foo(value, & : -> _) : String
|
||||
stub abstract def foo(value) : String
|
||||
stub abstract def foo(value, & : -> _) : String
|
||||
end
|
||||
|
||||
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
|
||||
|
@ -301,7 +297,7 @@ Spectator.describe Spectator::Double do
|
|||
arg
|
||||
end
|
||||
|
||||
stub def self.baz(arg)
|
||||
stub def self.baz(arg, &)
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
@ -309,7 +305,7 @@ Spectator.describe Spectator::Double do
|
|||
subject(dbl) { ClassDouble }
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
|
||||
|
||||
after_each { dbl._spectator_clear_stubs }
|
||||
after { dbl._spectator_clear_stubs }
|
||||
|
||||
it "overrides an existing method" do
|
||||
expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override)
|
||||
|
@ -357,7 +353,7 @@ Spectator.describe Spectator::Double do
|
|||
end
|
||||
|
||||
describe "._spectator_clear_stubs" do
|
||||
before_each { dbl._spectator_define_stub(foo_stub) }
|
||||
before { dbl._spectator_define_stub(foo_stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub)
|
||||
|
@ -365,7 +361,7 @@ Spectator.describe Spectator::Double do
|
|||
end
|
||||
|
||||
describe "._spectator_calls" do
|
||||
before_each { dbl._spectator_clear_calls }
|
||||
before { dbl._spectator_clear_calls }
|
||||
|
||||
# Retrieves symbolic names of methods called on a double.
|
||||
def called_method_names(dbl)
|
||||
|
@ -440,7 +436,7 @@ Spectator.describe Spectator::Double do
|
|||
subject(dbl) { FooBarDouble.new }
|
||||
let(stub) { Spectator::ValueStub.new(:foo, 5) }
|
||||
|
||||
before_each { dbl._spectator_define_stub(stub) }
|
||||
before { dbl._spectator_define_stub(stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42)
|
||||
|
@ -451,7 +447,7 @@ Spectator.describe Spectator::Double do
|
|||
subject(dbl) { FooBarDouble.new }
|
||||
let(stub) { Spectator::ValueStub.new(:foo, 5) }
|
||||
|
||||
before_each { dbl._spectator_define_stub(stub) }
|
||||
before { dbl._spectator_define_stub(stub) }
|
||||
|
||||
# Retrieves symbolic names of methods called on a double.
|
||||
def called_method_names(dbl)
|
||||
|
@ -469,7 +465,7 @@ Spectator.describe Spectator::Double do
|
|||
|
||||
it "stores calls to non-stubbed methods" do
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
expect(called_method_names(dbl)).to eq(%i[baz])
|
||||
expect(called_method_names(dbl)).to contain(:baz)
|
||||
end
|
||||
|
||||
it "stores arguments for a call" do
|
||||
|
@ -479,4 +475,68 @@ Spectator.describe Spectator::Double do
|
|||
expect(call.arguments).to eq(args)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_s" do
|
||||
subject(string) { dbl.to_s }
|
||||
|
||||
context "with a name" do
|
||||
let(dbl) { FooBarDouble.new }
|
||||
|
||||
it "indicates it's a double" do
|
||||
expect(string).to contain("Double")
|
||||
end
|
||||
|
||||
it "contains the double name" do
|
||||
expect(string).to contain("dbl-name")
|
||||
end
|
||||
end
|
||||
|
||||
context "without a name" do
|
||||
let(dbl) { EmptyDouble.new }
|
||||
|
||||
it "indicates it's a double" do
|
||||
expect(string).to contain("Double")
|
||||
end
|
||||
|
||||
it "contains \"Anonymous\"" do
|
||||
expect(string).to contain("Anonymous")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#inspect" do
|
||||
subject(string) { dbl.inspect }
|
||||
|
||||
context "with a name" do
|
||||
let(dbl) { FooBarDouble.new }
|
||||
|
||||
it "indicates it's a double" do
|
||||
expect(string).to contain("Double")
|
||||
end
|
||||
|
||||
it "contains the double name" do
|
||||
expect(string).to contain("dbl-name")
|
||||
end
|
||||
|
||||
it "contains the object ID" do
|
||||
expect(string).to contain(dbl.object_id.to_s(16))
|
||||
end
|
||||
end
|
||||
|
||||
context "without a name" do
|
||||
let(dbl) { EmptyDouble.new }
|
||||
|
||||
it "indicates it's a double" do
|
||||
expect(string).to contain("Double")
|
||||
end
|
||||
|
||||
it "contains \"Anonymous\"" do
|
||||
expect(string).to contain("Anonymous")
|
||||
end
|
||||
|
||||
it "contains the object ID" do
|
||||
expect(string).to contain(dbl.object_id.to_s(16))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
325
spec/spectator/mocks/formal_arguments_spec.cr
Normal file
325
spec/spectator/mocks/formal_arguments_spec.cr
Normal file
|
@ -0,0 +1,325 @@
|
|||
require "../../spec_helper"
|
||||
|
||||
Spectator.describe Spectator::FormalArguments do
|
||||
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "stores the arguments" do
|
||||
expect(arguments).to have_attributes(
|
||||
args: {arg1: 42, arg2: "foo"},
|
||||
splat_name: :splat,
|
||||
splat: {:x, :y, :z},
|
||||
kwargs: {bar: "baz", qux: 123}
|
||||
)
|
||||
end
|
||||
|
||||
describe ".build" do
|
||||
subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "stores the arguments and keyword arguments" do
|
||||
is_expected.to have_attributes(
|
||||
args: {arg1: 42, arg2: "foo"},
|
||||
splat_name: :splat,
|
||||
splat: {1, 2, 3},
|
||||
kwargs: {bar: "baz", qux: 123}
|
||||
)
|
||||
end
|
||||
|
||||
context "without a splat" do
|
||||
subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "stores the arguments and keyword arguments" do
|
||||
is_expected.to have_attributes(
|
||||
args: {arg1: 42, arg2: "foo"},
|
||||
splat: nil,
|
||||
kwargs: {bar: "baz", qux: 123}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#[](index)" do
|
||||
it "returns a positional argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[0]).to eq(42)
|
||||
expect(arguments[1]).to eq("foo")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns splat arguments" do
|
||||
aggregate_failures do
|
||||
expect(arguments[2]).to eq(:x)
|
||||
expect(arguments[3]).to eq(:y)
|
||||
expect(arguments[4]).to eq(:z)
|
||||
end
|
||||
end
|
||||
|
||||
context "with named positional arguments" do
|
||||
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "returns a positional argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[0]).to eq(42)
|
||||
expect(arguments[1]).to eq("foo")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns splat arguments" do
|
||||
aggregate_failures do
|
||||
expect(arguments[2]).to eq(:x)
|
||||
expect(arguments[3]).to eq(:y)
|
||||
expect(arguments[4]).to eq(:z)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#[](symbol)" do
|
||||
it "returns a keyword argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[:bar]).to eq("baz")
|
||||
expect(arguments[:qux]).to eq(123)
|
||||
end
|
||||
end
|
||||
|
||||
context "with named positional arguments" do
|
||||
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "returns a positional argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[:arg1]).to eq(42)
|
||||
expect(arguments[:arg2]).to eq("foo")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns a keyword argument" do
|
||||
aggregate_failures do
|
||||
expect(arguments[:bar]).to eq("baz")
|
||||
expect(arguments[:qux]).to eq(123)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_s" do
|
||||
subject { arguments.to_s }
|
||||
|
||||
it "formats the arguments" do
|
||||
is_expected.to eq("(arg1: 42, arg2: \"foo\", *splat: {:x, :y, :z}, bar: \"baz\", qux: 123)")
|
||||
end
|
||||
|
||||
context "when empty" do
|
||||
let(arguments) { Spectator::FormalArguments.none }
|
||||
|
||||
it "returns (no args)" do
|
||||
is_expected.to eq("(no args)")
|
||||
end
|
||||
end
|
||||
|
||||
context "with a splat and no arguments" do
|
||||
let(arguments) { Spectator::FormalArguments.build(NamedTuple.new, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it "omits the splat name" do
|
||||
is_expected.to eq("(:x, :y, :z, bar: \"baz\", qux: 123)")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#==" do
|
||||
subject { arguments == other }
|
||||
|
||||
context "with Arguments" do
|
||||
context "with equal arguments" do
|
||||
let(other) { Spectator::Arguments.new(arguments.positional, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(other) { Spectator::Arguments.new(arguments.positional, {qux: 123, bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with an extra kwarg" do
|
||||
let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz", qux: 123, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
end
|
||||
|
||||
context "with FormalArguments" do
|
||||
context "with equal arguments" do
|
||||
let(other) { arguments }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with an extra kwarg" do
|
||||
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different splat arguments" do
|
||||
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with mixed positional tuple types" do
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with mixed positional tuple types (flipped)" do
|
||||
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#===" do
|
||||
subject { pattern === arguments }
|
||||
|
||||
context "with Arguments" do
|
||||
let(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
context "with equal arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with matching arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, {bar: /foo/, qux: "123"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {qux: Int32, bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with an additional kwarg" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
end
|
||||
|
||||
context "with FormalArguments" do
|
||||
context "with equal arguments" do
|
||||
let(pattern) { arguments }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with matching arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with the same kwargs in a different order" do
|
||||
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with an additional kwarg" do
|
||||
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with a missing kwarg" do
|
||||
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with different splat arguments" do
|
||||
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
|
||||
context "with matching mixed positional tuple types" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_true }
|
||||
end
|
||||
|
||||
context "with non-matching mixed positional tuple types" do
|
||||
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
|
||||
|
||||
it { is_expected.to be_false }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -235,16 +235,9 @@ Spectator.describe Spectator::LazyDouble do
|
|||
end
|
||||
|
||||
context "with previously undefined methods" do
|
||||
it "can stub methods" do
|
||||
it "raises an error" do
|
||||
stub = Spectator::ValueStub.new(:baz, :xyz)
|
||||
dbl._spectator_define_stub(stub)
|
||||
expect(dbl.baz).to eq(:xyz)
|
||||
end
|
||||
|
||||
it "uses a stub only if an argument constraint is met" do
|
||||
stub = Spectator::ValueStub.new(:baz, :xyz, Spectator::Arguments.capture(:right))
|
||||
dbl._spectator_define_stub(stub)
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
expect { dbl._spectator_define_stub(stub) }.to raise_error(/stub/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -253,27 +246,18 @@ Spectator.describe Spectator::LazyDouble do
|
|||
subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") }
|
||||
let(stub) { Spectator::ValueStub.new(:foo, 5) }
|
||||
|
||||
before_each { dbl._spectator_define_stub(stub) }
|
||||
before { dbl._spectator_define_stub(stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42)
|
||||
end
|
||||
|
||||
it "raises on methods without an implementation" do
|
||||
stub = Spectator::ValueStub.new(:baz, :xyz)
|
||||
dbl._spectator_define_stub(stub)
|
||||
expect(dbl.baz).to eq(:xyz)
|
||||
|
||||
dbl._spectator_clear_stubs
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#_spectator_calls" do
|
||||
subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") }
|
||||
let(stub) { Spectator::ValueStub.new(:foo, 5) }
|
||||
|
||||
before_each { dbl._spectator_define_stub(stub) }
|
||||
before { dbl._spectator_define_stub(stub) }
|
||||
|
||||
# Retrieves symbolic names of methods called on a double.
|
||||
def called_method_names(dbl)
|
||||
|
@ -291,7 +275,7 @@ Spectator.describe Spectator::LazyDouble do
|
|||
|
||||
it "stores calls to non-stubbed methods" do
|
||||
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
|
||||
expect(called_method_names(dbl)).to eq(%i[baz])
|
||||
expect(called_method_names(dbl)).to contain(:baz)
|
||||
end
|
||||
|
||||
it "stores arguments for a call" do
|
||||
|
@ -301,4 +285,68 @@ Spectator.describe Spectator::LazyDouble do
|
|||
expect(call.arguments).to eq(args)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_s" do
|
||||
subject(string) { dbl.to_s }
|
||||
|
||||
context "with a name" do
|
||||
let(dbl) { Spectator::LazyDouble.new("dbl-name") }
|
||||
|
||||
it "indicates it's a double" do
|
||||
expect(string).to contain("LazyDouble")
|
||||
end
|
||||
|
||||
it "contains the double name" do
|
||||
expect(string).to contain("dbl-name")
|
||||
end
|
||||
end
|
||||
|
||||
context "without a name" do
|
||||
let(dbl) { Spectator::LazyDouble.new }
|
||||
|
||||
it "contains the double type" do
|
||||
expect(string).to contain("LazyDouble")
|
||||
end
|
||||
|
||||
it "contains \"Anonymous\"" do
|
||||
expect(string).to contain("Anonymous")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#inspect" do
|
||||
subject(string) { dbl.inspect }
|
||||
|
||||
context "with a name" do
|
||||
let(dbl) { Spectator::LazyDouble.new("dbl-name") }
|
||||
|
||||
it "contains the double type" do
|
||||
expect(string).to contain("LazyDouble")
|
||||
end
|
||||
|
||||
it "contains the double name" do
|
||||
expect(string).to contain("dbl-name")
|
||||
end
|
||||
|
||||
it "contains the object ID" do
|
||||
expect(string).to contain(dbl.object_id.to_s(16))
|
||||
end
|
||||
end
|
||||
|
||||
context "without a name" do
|
||||
let(dbl) { Spectator::LazyDouble.new }
|
||||
|
||||
it "contains the double type" do
|
||||
expect(string).to contain("LazyDouble")
|
||||
end
|
||||
|
||||
it "contains \"Anonymous\"" do
|
||||
expect(string).to contain("Anonymous")
|
||||
end
|
||||
|
||||
it "contains the object ID" do
|
||||
expect(string).to contain(dbl.object_id.to_s(16))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,8 +29,18 @@ Spectator.describe Spectator::Mock do
|
|||
@_spectator_invocations << :method3
|
||||
"original"
|
||||
end
|
||||
|
||||
def method4 : Thing
|
||||
self
|
||||
end
|
||||
|
||||
def method5 : OtherThing
|
||||
OtherThing.new
|
||||
end
|
||||
end
|
||||
|
||||
class OtherThing; end
|
||||
|
||||
Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do
|
||||
stub def method2
|
||||
:stubbed
|
||||
|
@ -104,6 +114,20 @@ Spectator.describe Spectator::Mock do
|
|||
mock.method3
|
||||
expect(mock._spectator_invocations).to contain_exactly(:method3)
|
||||
end
|
||||
|
||||
it "can reference its own type" do
|
||||
new_mock = MockThing.new
|
||||
stub = Spectator::ValueStub.new(:method4, new_mock)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.method4).to be(new_mock)
|
||||
end
|
||||
|
||||
it "can reference other types in the original namespace" do
|
||||
other = OtherThing.new
|
||||
stub = Spectator::ValueStub.new(:method5, other)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.method5).to be(other)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an abstract class" do
|
||||
|
@ -120,8 +144,14 @@ Spectator.describe Spectator::Mock do
|
|||
end
|
||||
|
||||
abstract def method4
|
||||
|
||||
abstract def method4 : Thing
|
||||
|
||||
abstract def method5 : OtherThing
|
||||
end
|
||||
|
||||
class OtherThing; end
|
||||
|
||||
Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method2: :stubbed) do
|
||||
stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent.
|
||||
123
|
||||
|
@ -199,6 +229,20 @@ Spectator.describe Spectator::Mock do
|
|||
mock.method3
|
||||
expect(mock._spectator_invocations).to contain_exactly(:method3)
|
||||
end
|
||||
|
||||
it "can reference its own type" do
|
||||
new_mock = MockThing.new
|
||||
stub = Spectator::ValueStub.new(:method4, new_mock)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.method4).to be(new_mock)
|
||||
end
|
||||
|
||||
it "can reference other types in the original namespace" do
|
||||
other = OtherThing.new
|
||||
stub = Spectator::ValueStub.new(:method5, other)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.method5).to be(other)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an abstract struct" do
|
||||
|
@ -215,8 +259,14 @@ Spectator.describe Spectator::Mock do
|
|||
end
|
||||
|
||||
abstract def method4
|
||||
|
||||
abstract def method4 : Thing
|
||||
|
||||
abstract def method5 : OtherThing
|
||||
end
|
||||
|
||||
class OtherThing; end
|
||||
|
||||
Spectator::Mock.define_subtype(:struct, Thing, MockThing, :mock_name, method2: :stubbed) do
|
||||
stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent.
|
||||
123
|
||||
|
@ -286,6 +336,22 @@ Spectator.describe Spectator::Mock do
|
|||
mock.method3
|
||||
expect(mock._spectator_invocations).to contain_exactly(:method3)
|
||||
end
|
||||
|
||||
it "can reference its own type" do
|
||||
mock = self.mock # FIXME: Workaround for passing by value messing with stubs.
|
||||
new_mock = MockThing.new
|
||||
stub = Spectator::ValueStub.new(:method4, new_mock)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.method4).to be_a(Thing)
|
||||
end
|
||||
|
||||
it "can reference other types in the original namespace" do
|
||||
mock = self.mock # FIXME: Workaround for passing by value messing with stubs.
|
||||
other = OtherThing.new
|
||||
stub = Spectator::ValueStub.new(:method5, other)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.method5).to be(other)
|
||||
end
|
||||
end
|
||||
|
||||
context "class method stubs" do
|
||||
|
@ -298,11 +364,21 @@ Spectator.describe Spectator::Mock do
|
|||
arg
|
||||
end
|
||||
|
||||
def self.baz(arg)
|
||||
def self.baz(arg, &)
|
||||
yield
|
||||
end
|
||||
|
||||
def self.thing : Thing
|
||||
new
|
||||
end
|
||||
|
||||
def self.other : OtherThing
|
||||
OtherThing.new
|
||||
end
|
||||
end
|
||||
|
||||
class OtherThing; end
|
||||
|
||||
Spectator::Mock.define_subtype(:class, Thing, MockThing) do
|
||||
stub def self.foo
|
||||
:stub
|
||||
|
@ -312,7 +388,7 @@ Spectator.describe Spectator::Mock do
|
|||
let(mock) { MockThing }
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
|
||||
|
||||
after_each { mock._spectator_clear_stubs }
|
||||
after { mock._spectator_clear_stubs }
|
||||
|
||||
it "overrides an existing method" do
|
||||
expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override)
|
||||
|
@ -367,8 +443,22 @@ Spectator.describe Spectator::Mock do
|
|||
expect(restricted(mock)).to eq(:stub)
|
||||
end
|
||||
|
||||
it "can reference its own type" do
|
||||
new_mock = MockThing.new
|
||||
stub = Spectator::ValueStub.new(:thing, new_mock)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.thing).to be(new_mock)
|
||||
end
|
||||
|
||||
it "can reference other types in the original namespace" do
|
||||
other = OtherThing.new
|
||||
stub = Spectator::ValueStub.new(:other, other)
|
||||
mock._spectator_define_stub(stub)
|
||||
expect(mock.other).to be(other)
|
||||
end
|
||||
|
||||
describe "._spectator_clear_stubs" do
|
||||
before_each { mock._spectator_define_stub(foo_stub) }
|
||||
before { mock._spectator_define_stub(foo_stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub)
|
||||
|
@ -376,7 +466,7 @@ Spectator.describe Spectator::Mock do
|
|||
end
|
||||
|
||||
describe "._spectator_calls" do
|
||||
before_each { mock._spectator_clear_calls }
|
||||
before { mock._spectator_clear_calls }
|
||||
|
||||
# Retrieves symbolic names of methods called on a mock.
|
||||
def called_method_names(mock)
|
||||
|
@ -401,6 +491,203 @@ Spectator.describe Spectator::Mock do
|
|||
end
|
||||
end
|
||||
|
||||
context "with a module" do
|
||||
module Thing
|
||||
# `extend self` cannot be used.
|
||||
# The Crystal compiler doesn't report the methods as class methods when doing so.
|
||||
|
||||
def self.original_method
|
||||
:original
|
||||
end
|
||||
|
||||
def self.default_method
|
||||
:original
|
||||
end
|
||||
|
||||
def self.stubbed_method(_value = 42)
|
||||
:original
|
||||
end
|
||||
end
|
||||
|
||||
Spectator::Mock.define_subtype(:module, Thing, MockThing) do
|
||||
stub def self.stubbed_method(_value = 42)
|
||||
:stubbed
|
||||
end
|
||||
end
|
||||
|
||||
let(mock) { MockThing }
|
||||
|
||||
after { mock._spectator_clear_stubs }
|
||||
|
||||
it "overrides an existing method" do
|
||||
stub = Spectator::ValueStub.new(:original_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override)
|
||||
end
|
||||
|
||||
it "doesn't affect other methods" do
|
||||
stub = Spectator::ValueStub.new(:stubbed_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method }
|
||||
end
|
||||
|
||||
it "replaces an existing default stub" do
|
||||
stub = Spectator::ValueStub.new(:default_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override)
|
||||
end
|
||||
|
||||
it "replaces an existing stubbed method" do
|
||||
stub = Spectator::ValueStub.new(:stubbed_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override)
|
||||
end
|
||||
|
||||
def restricted(thing : Thing.class)
|
||||
thing.stubbed_method
|
||||
end
|
||||
|
||||
it "can be used in type restricted methods" do
|
||||
expect(restricted(mock)).to eq(:stubbed)
|
||||
end
|
||||
|
||||
describe "._spectator_clear_stubs" do
|
||||
before do
|
||||
stub = Spectator::ValueStub.new(:original_method, :override)
|
||||
mock._spectator_define_stub(stub)
|
||||
end
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original)
|
||||
end
|
||||
end
|
||||
|
||||
describe "._spectator_calls" do
|
||||
before { mock._spectator_clear_calls }
|
||||
|
||||
# Retrieves symbolic names of methods called on a mock.
|
||||
def called_method_names(mock)
|
||||
mock._spectator_calls.map(&.method)
|
||||
end
|
||||
|
||||
it "stores calls to original methods" do
|
||||
expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method])
|
||||
end
|
||||
|
||||
it "stores calls to default methods" do
|
||||
expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method])
|
||||
end
|
||||
|
||||
it "stores calls to stubbed methods" do
|
||||
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method])
|
||||
end
|
||||
|
||||
it "stores multiple calls to the same stub" do
|
||||
mock.stubbed_method
|
||||
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method])
|
||||
end
|
||||
|
||||
it "stores arguments for a call" do
|
||||
mock.stubbed_method(5)
|
||||
args = Spectator::Arguments.capture(5)
|
||||
call = mock._spectator_calls.first
|
||||
expect(call.arguments).to eq(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a mocked module included in a class" do
|
||||
module Thing
|
||||
def original_method
|
||||
:original
|
||||
end
|
||||
|
||||
def default_method
|
||||
:original
|
||||
end
|
||||
|
||||
def stubbed_method(_value = 42)
|
||||
:original
|
||||
end
|
||||
end
|
||||
|
||||
Spectator::Mock.define_subtype(:module, Thing, MockThing, default_method: :default) do
|
||||
stub def stubbed_method(_value = 42)
|
||||
:stubbed
|
||||
end
|
||||
end
|
||||
|
||||
class IncludedMock
|
||||
include MockThing
|
||||
end
|
||||
|
||||
let(mock) { IncludedMock.new }
|
||||
|
||||
it "overrides an existing method" do
|
||||
stub = Spectator::ValueStub.new(:original_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override)
|
||||
end
|
||||
|
||||
it "doesn't affect other methods" do
|
||||
stub = Spectator::ValueStub.new(:stubbed_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method }
|
||||
end
|
||||
|
||||
it "replaces an existing default stub" do
|
||||
stub = Spectator::ValueStub.new(:default_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override)
|
||||
end
|
||||
|
||||
it "replaces an existing stubbed method" do
|
||||
stub = Spectator::ValueStub.new(:stubbed_method, :override)
|
||||
expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override)
|
||||
end
|
||||
|
||||
def restricted(thing : Thing.class)
|
||||
thing.default_method
|
||||
end
|
||||
|
||||
describe "#_spectator_clear_stubs" do
|
||||
before do
|
||||
stub = Spectator::ValueStub.new(:original_method, :override)
|
||||
mock._spectator_define_stub(stub)
|
||||
end
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#_spectator_calls" do
|
||||
before { mock._spectator_clear_calls }
|
||||
|
||||
# Retrieves symbolic names of methods called on a mock.
|
||||
def called_method_names(mock)
|
||||
mock._spectator_calls.map(&.method)
|
||||
end
|
||||
|
||||
it "stores calls to original methods" do
|
||||
expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method])
|
||||
end
|
||||
|
||||
it "stores calls to default methods" do
|
||||
expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method])
|
||||
end
|
||||
|
||||
it "stores calls to stubbed methods" do
|
||||
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method])
|
||||
end
|
||||
|
||||
it "stores multiple calls to the same stub" do
|
||||
mock.stubbed_method
|
||||
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method])
|
||||
end
|
||||
|
||||
it "stores arguments for a call" do
|
||||
mock.stubbed_method(5)
|
||||
args = Spectator::Arguments.capture(5)
|
||||
call = mock._spectator_calls.first
|
||||
expect(call.arguments).to eq(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a method that uses NoReturn" do
|
||||
abstract class Thing
|
||||
abstract def oops : NoReturn
|
||||
|
@ -410,7 +697,7 @@ Spectator.describe Spectator::Mock do
|
|||
|
||||
let(mock) { MockThing.new }
|
||||
|
||||
after_each { mock._spectator_clear_stubs }
|
||||
after { mock._spectator_clear_stubs }
|
||||
|
||||
it "raises a TypeCastError when using a value-based stub" do
|
||||
stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub)
|
||||
|
@ -461,7 +748,7 @@ Spectator.describe Spectator::Mock do
|
|||
let(mock) { MockedClass.new }
|
||||
|
||||
# Necessary to clear stubs to prevent leakages between tests.
|
||||
after_each { mock._spectator_clear_stubs }
|
||||
after { mock._spectator_clear_stubs }
|
||||
|
||||
it "overrides responses from methods with keyword arguments" do
|
||||
expect(mock.method1).to eq(123)
|
||||
|
@ -571,8 +858,8 @@ Spectator.describe Spectator::Mock do
|
|||
let(mock) { MockedStruct.new }
|
||||
|
||||
# Necessary to clear stubs to prevent leakages between tests.
|
||||
after_each { mock._spectator_clear_stubs }
|
||||
after_each { MockedStruct._spectator_invocations.clear }
|
||||
after { mock._spectator_clear_stubs }
|
||||
after { MockedStruct._spectator_invocations.clear }
|
||||
|
||||
it "overrides responses from methods with keyword arguments" do
|
||||
expect(mock.method1).to eq(123)
|
||||
|
@ -642,7 +929,7 @@ Spectator.describe Spectator::Mock do
|
|||
arg
|
||||
end
|
||||
|
||||
def self.baz(arg)
|
||||
def self.baz(arg, &)
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
@ -656,7 +943,7 @@ Spectator.describe Spectator::Mock do
|
|||
let(mock) { Thing }
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
|
||||
|
||||
after_each { mock._spectator_clear_stubs }
|
||||
after { mock._spectator_clear_stubs }
|
||||
|
||||
it "overrides an existing method" do
|
||||
expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override)
|
||||
|
@ -712,7 +999,7 @@ Spectator.describe Spectator::Mock do
|
|||
end
|
||||
|
||||
describe "._spectator_clear_stubs" do
|
||||
before_each { mock._spectator_define_stub(foo_stub) }
|
||||
before { mock._spectator_define_stub(foo_stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub)
|
||||
|
@ -720,7 +1007,7 @@ Spectator.describe Spectator::Mock do
|
|||
end
|
||||
|
||||
describe "._spectator_calls" do
|
||||
before_each { mock._spectator_clear_calls }
|
||||
before { mock._spectator_clear_calls }
|
||||
|
||||
# Retrieves symbolic names of methods called on a mock.
|
||||
def called_method_names(mock)
|
||||
|
@ -756,7 +1043,7 @@ Spectator.describe Spectator::Mock do
|
|||
|
||||
let(mock) { NoReturnThing.new }
|
||||
|
||||
after_each { mock._spectator_clear_stubs }
|
||||
after { mock._spectator_clear_stubs }
|
||||
|
||||
it "raises a TypeCastError when using a value-based stub" do
|
||||
stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub)
|
||||
|
|
|
@ -50,7 +50,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "with abstract stubs and return type annotations" do
|
||||
Spectator::NullDouble.define(TestDouble2) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
stub abstract def foo(value) : String
|
||||
end
|
||||
|
||||
let(arguments) { Spectator::Arguments.capture(/foo/) }
|
||||
|
@ -74,8 +74,8 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "with nillable return type annotations" do
|
||||
Spectator::NullDouble.define(TestDouble) do
|
||||
abstract_stub abstract def foo : String?
|
||||
abstract_stub abstract def bar : Nil
|
||||
stub abstract def foo : String?
|
||||
stub abstract def bar : Nil
|
||||
end
|
||||
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, nil).as(Spectator::Stub) }
|
||||
|
@ -92,7 +92,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "with a method that uses NoReturn" do
|
||||
Spectator::NullDouble.define(NoReturnDouble) do
|
||||
abstract_stub abstract def oops : NoReturn
|
||||
stub abstract def oops : NoReturn
|
||||
end
|
||||
|
||||
subject(dbl) { NoReturnDouble.new }
|
||||
|
@ -186,12 +186,9 @@ Spectator.describe Spectator::NullDouble do
|
|||
expect(dbl.hash).to be_a(UInt64)
|
||||
expect(dbl.in?([42])).to be_false
|
||||
expect(dbl.in?(1, 2, 3)).to be_false
|
||||
expect(dbl.inspect).to contain("EmptyDouble")
|
||||
expect(dbl.itself).to be(dbl)
|
||||
expect(dbl.not_nil!).to be(dbl)
|
||||
expect(dbl.pretty_inspect).to contain("EmptyDouble")
|
||||
expect(dbl.tap { nil }).to be(dbl)
|
||||
expect(dbl.to_s).to contain("EmptyDouble")
|
||||
expect(dbl.try { nil }).to be_nil
|
||||
expect(dbl.object_id).to be_a(UInt64)
|
||||
expect(dbl.same?(dbl)).to be_true
|
||||
|
@ -205,8 +202,8 @@ Spectator.describe Spectator::NullDouble do
|
|||
|
||||
context "without common object methods" do
|
||||
Spectator::NullDouble.define(TestDouble) do
|
||||
abstract_stub abstract def foo(value) : String
|
||||
abstract_stub abstract def foo(value, & : -> _) : String
|
||||
stub abstract def foo(value) : String
|
||||
stub abstract def foo(value, & : -> _) : String
|
||||
end
|
||||
|
||||
let(stub) { Spectator::ValueStub.new(:foo, "bar", arguments).as(Spectator::Stub) }
|
||||
|
@ -262,7 +259,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
arg
|
||||
end
|
||||
|
||||
stub def self.baz(arg)
|
||||
stub def self.baz(arg, &)
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
@ -270,7 +267,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
subject(dbl) { ClassDouble }
|
||||
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
|
||||
|
||||
after_each { dbl._spectator_clear_stubs }
|
||||
after { dbl._spectator_clear_stubs }
|
||||
|
||||
it "overrides an existing method" do
|
||||
expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override)
|
||||
|
@ -318,7 +315,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
end
|
||||
|
||||
describe "._spectator_clear_stubs" do
|
||||
before_each { dbl._spectator_define_stub(foo_stub) }
|
||||
before { dbl._spectator_define_stub(foo_stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub)
|
||||
|
@ -326,7 +323,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
end
|
||||
|
||||
describe "._spectator_calls" do
|
||||
before_each { dbl._spectator_clear_calls }
|
||||
before { dbl._spectator_clear_calls }
|
||||
|
||||
# Retrieves symbolic names of methods called on a double.
|
||||
def called_method_names(dbl)
|
||||
|
@ -401,7 +398,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
subject(dbl) { FooBarDouble.new }
|
||||
let(stub) { Spectator::ValueStub.new(:foo, 5) }
|
||||
|
||||
before_each { dbl._spectator_define_stub(stub) }
|
||||
before { dbl._spectator_define_stub(stub) }
|
||||
|
||||
it "removes previously defined stubs" do
|
||||
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42)
|
||||
|
@ -412,7 +409,7 @@ Spectator.describe Spectator::NullDouble do
|
|||
subject(dbl) { FooBarDouble.new }
|
||||
let(stub) { Spectator::ValueStub.new(:foo, 5) }
|
||||
|
||||
before_each { dbl._spectator_define_stub(stub) }
|
||||
before { dbl._spectator_define_stub(stub) }
|
||||
|
||||
# Retrieves symbolic names of methods called on a double.
|
||||
def called_method_names(dbl)
|
||||
|
@ -439,4 +436,68 @@ Spectator.describe Spectator::NullDouble do
|
|||
expect(call.arguments).to eq(args)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_s" do
|
||||
subject(string) { dbl.to_s }
|
||||
|
||||
context "with a name" do
|
||||
let(dbl) { FooBarDouble.new }
|
||||
|
||||
it "indicates it's a double" do
|
||||
expect(string).to contain("NullDouble")
|
||||
end
|
||||
|
||||
it "contains the double name" do
|
||||
expect(string).to contain("dbl-name")
|
||||
end
|
||||
end
|
||||
|
||||
context "without a name" do
|
||||
let(dbl) { EmptyDouble.new }
|
||||
|
||||
it "contains the double type" do
|
||||
expect(string).to contain("NullDouble")
|
||||
end
|
||||
|
||||
it "contains \"Anonymous\"" do
|
||||
expect(string).to contain("Anonymous")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#inspect" do
|
||||
subject(string) { dbl.inspect }
|
||||
|
||||
context "with a name" do
|
||||
let(dbl) { FooBarDouble.new }
|
||||
|
||||
it "contains the double type" do
|
||||
expect(string).to contain("NullDouble")
|
||||
end
|
||||
|
||||
it "contains the double name" do
|
||||
expect(string).to contain("dbl-name")
|
||||
end
|
||||
|
||||
it "contains the object ID" do
|
||||
expect(string).to contain(dbl.object_id.to_s(16))
|
||||
end
|
||||
end
|
||||
|
||||
context "without a name" do
|
||||
let(dbl) { EmptyDouble.new }
|
||||
|
||||
it "contains the double type" do
|
||||
expect(string).to contain("NullDouble")
|
||||
end
|
||||
|
||||
it "contains \"Anonymous\"" do
|
||||
expect(string).to contain("Anonymous")
|
||||
end
|
||||
|
||||
it "contains the object ID" do
|
||||
expect(string).to contain(dbl.object_id.to_s(16))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require "colorize"
|
||||
require "log"
|
||||
require "mocks"
|
||||
require "./spectator/includes"
|
||||
|
||||
# Module that contains all functionality related to Spectator.
|
||||
|
|
|
@ -34,7 +34,7 @@ module Spectator
|
|||
|
||||
# Produces a string representation of the expression.
|
||||
# This consists of the label (if one is available) and the value.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
if (label = @label)
|
||||
io << label << ": "
|
||||
end
|
||||
|
@ -43,7 +43,7 @@ module Spectator
|
|||
|
||||
# Produces a detailed string representation of the expression.
|
||||
# This consists of the label (if one is available) and the value.
|
||||
def inspect(io)
|
||||
def inspect(io : IO) : Nil
|
||||
if (label = @label)
|
||||
io << label << ": "
|
||||
end
|
||||
|
|
|
@ -13,12 +13,12 @@ module Spectator
|
|||
end
|
||||
|
||||
# Displays "anything".
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "anything"
|
||||
end
|
||||
|
||||
# Displays "<anything>".
|
||||
def inspect(io)
|
||||
def inspect(io : IO) : Nil
|
||||
io << "<anything>"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -112,7 +112,7 @@ module Spectator
|
|||
# Adds the example filter option to the parser.
|
||||
private def example_option(parser, builder)
|
||||
parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern|
|
||||
Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" }
|
||||
Log.debug { "Filtering for examples containing '#{pattern}' (-e '#{pattern}')" }
|
||||
filter = NameNodeFilter.new(pattern)
|
||||
builder.add_node_filter(filter)
|
||||
end
|
||||
|
|
|
@ -4,18 +4,23 @@
|
|||
# This type is intentionally outside the `Spectator` module.
|
||||
# The reason for this is to prevent name collision when using the DSL to define a spec.
|
||||
abstract class SpectatorContext
|
||||
# Evaluates the contents of a block within the scope of the context.
|
||||
def eval(&)
|
||||
with self yield
|
||||
end
|
||||
|
||||
# Produces a dummy string to represent the context as a string.
|
||||
# This prevents the default behavior, which normally stringifies instance variables.
|
||||
# Due to the sheer amount of types Spectator can create
|
||||
# and that the Crystal compiler instantiates a `#to_s` and/or `#inspect` for each of those types,
|
||||
# an explosion in method instances can be created.
|
||||
# The compile time is drastically reduced by using a dummy string instead.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "Context"
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def inspect(io)
|
||||
def inspect(io : IO) : Nil
|
||||
io << "Context<" << self.class << '>'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -182,7 +182,7 @@ module Spectator::DSL
|
|||
# expect(false).to be_true
|
||||
# end
|
||||
# ```
|
||||
def aggregate_failures(label = nil)
|
||||
def aggregate_failures(label = nil, &)
|
||||
::Spectator::Harness.current.aggregate_failures(label) do
|
||||
yield
|
||||
end
|
||||
|
|
|
@ -137,7 +137,11 @@ module Spectator::DSL
|
|||
what.is_a?(NilLiteral) %}
|
||||
{{what}}
|
||||
{% elsif what.is_a?(StringInterpolation) %}
|
||||
{% raise "String interpolation isn't supported for example group names" %}
|
||||
{{@type.name}}.new.eval do
|
||||
{{what}}
|
||||
rescue e
|
||||
"<Failed to evaluate context label - #{e.class}: #{e}>"
|
||||
end
|
||||
{% else %}
|
||||
{{what.stringify}}
|
||||
{% end %}
|
||||
|
|
|
@ -124,11 +124,21 @@ module Spectator::DSL
|
|||
# This means that values defined by `let` and `subject` are available.
|
||||
define_example_hook :before_each
|
||||
|
||||
# :ditto:
|
||||
macro before(&block)
|
||||
before_each {{block}}
|
||||
end
|
||||
|
||||
# Defines a block of code that will be invoked after every example in the group.
|
||||
# The block will be run in the context of the current running example.
|
||||
# This means that values defined by `let` and `subject` are available.
|
||||
define_example_hook :after_each
|
||||
|
||||
# :ditto:
|
||||
macro after(&block)
|
||||
after_each {{block}}
|
||||
end
|
||||
|
||||
# Defines a block of code that will be invoked around every example in the group.
|
||||
# The block will be run in the context of the current running example.
|
||||
# This means that values defined by `let` and `subject` are available.
|
||||
|
@ -139,6 +149,11 @@ module Spectator::DSL
|
|||
# More code can run afterwards (in the block).
|
||||
define_example_hook :around_each
|
||||
|
||||
# :ditto:
|
||||
macro around(&block)
|
||||
around_each {{block}}
|
||||
end
|
||||
|
||||
# Defines a block of code that will be invoked before every example in the group.
|
||||
# The block will be run in the context of the current running example.
|
||||
# This means that values defined by `let` and `subject` are available.
|
||||
|
|
|
@ -790,7 +790,7 @@ module Spectator::DSL
|
|||
# ```
|
||||
# expect_raises { raise "foobar" }
|
||||
# ```
|
||||
macro expect_raises
|
||||
macro expect_raises(&block)
|
||||
expect {{block}}.to raise_error
|
||||
end
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ module Spectator::DSL
|
|||
private macro _spectator_metadata(name, source, *tags, **metadata)
|
||||
private def self.{{name.id}}
|
||||
%metadata = {{source.id}}.dup
|
||||
{% unless tags.empty? && metadata.empty? %}
|
||||
%metadata ||= ::Spectator::Metadata.new
|
||||
{% end %}
|
||||
{% for k in tags %}
|
||||
%metadata[{{k.id.symbolize}}] = nil
|
||||
{% end %}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
require "../mocks"
|
||||
require "mocks/dsl/allow_syntax"
|
||||
|
||||
module Spectator::DSL
|
||||
# Methods and macros for mocks and doubles.
|
||||
module Mocks
|
||||
include ::Mocks::DSL::AllowSyntax
|
||||
|
||||
# All defined double and mock types.
|
||||
# Each tuple consists of the double name or mocked type,
|
||||
# defined context (example group), and double type name relative to its context.
|
||||
|
@ -31,20 +33,9 @@ module Spectator::DSL
|
|||
::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %}
|
||||
|
||||
# Define the plain double type.
|
||||
::Spectator::Double.define({{double_type_name}}, {{name}}, {{**value_methods}}) do
|
||||
# Returns a new double that responds to undefined methods with itself.
|
||||
# See: `NullDouble`
|
||||
def as_null_object
|
||||
{{null_double_type_name}}.new(@stubs)
|
||||
end
|
||||
|
||||
{% if block %}{{block.body}}{% end %}
|
||||
::Mocks::Double.define({{double_type_name}}, {{**value_methods}}) do
|
||||
{{block.body if block}}
|
||||
end
|
||||
|
||||
{% begin %}
|
||||
# Define a matching null double type.
|
||||
::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Instantiates a double.
|
||||
|
@ -94,11 +85,11 @@ module Spectator::DSL
|
|||
|
||||
begin
|
||||
%double = {% if found_tuple %}
|
||||
{{found_tuple[2].id}}.new({{**value_methods}})
|
||||
{{found_tuple[2].id}}.new({{found_tuple[0].id.stringify}}, {{**value_methods}})
|
||||
{% else %}
|
||||
::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset })
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double.__mocks.reset })
|
||||
%double
|
||||
end
|
||||
end
|
||||
|
@ -162,7 +153,7 @@ module Spectator::DSL
|
|||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%double._spectator_define_stub(%stub{key})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset })
|
||||
::Spectator::Harness.current?.try(&.cleanup { %double.__mocks.reset })
|
||||
%double
|
||||
end
|
||||
end
|
||||
|
@ -218,24 +209,29 @@ module Spectator::DSL
|
|||
# end
|
||||
# ```
|
||||
private macro def_mock(type, name = nil, **value_methods, &block)
|
||||
{% # Construct a unique type name for the mock by using the number of defined types.
|
||||
index = ::Spectator::DSL::Mocks::TYPES.size
|
||||
mock_type_name = "Mock#{index}".id
|
||||
{% resolved = type.resolve
|
||||
# Construct a unique type name for the mock by using the number of defined types.
|
||||
index = ::Spectator::DSL::Mocks::TYPES.size
|
||||
# The type is nested under the original so that any type names from the original can be resolved.
|
||||
mock_type_name = "Mock#{index}".id
|
||||
|
||||
# Store information about how the mock is defined and its context.
|
||||
# This is important for constructing an instance of the mock later.
|
||||
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, mock_type_name.symbolize}
|
||||
# Store information about how the mock is defined and its context.
|
||||
# This is important for constructing an instance of the mock later.
|
||||
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "::#{resolved.name}::#{mock_type_name}".id.symbolize}
|
||||
|
||||
resolved = type.resolve
|
||||
base = if resolved.class?
|
||||
:class
|
||||
elsif resolved.struct?
|
||||
:struct
|
||||
else
|
||||
:module
|
||||
end %}
|
||||
base = if resolved.class?
|
||||
:class
|
||||
elsif resolved.struct?
|
||||
:struct
|
||||
else
|
||||
:module
|
||||
end %}
|
||||
|
||||
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
|
||||
{% begin %}
|
||||
{{base.id}} ::{{resolved.name}}
|
||||
::Mocks::Mock.define({{mock_type_name}} < ::{{resolved.name}}, {{**value_methods}}) {{block}}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Instantiates a mock.
|
||||
|
@ -296,10 +292,10 @@ module Spectator::DSL
|
|||
{% if found_tuple %}
|
||||
{{found_tuple[2].id}}.new.tap do |%mock|
|
||||
{% for key, value in value_methods %}
|
||||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock._spectator_define_stub(%stub{key})
|
||||
%stub{key} = ::Mocks::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock.__mocks.add_stub(%stub{key})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset })
|
||||
::Spectator::Harness.current?.try(&.cleanup { %mock.__mocks.reset })
|
||||
end
|
||||
{% else %}
|
||||
{% raise "Type `#{type.id}` must be previously mocked before attempting to instantiate." %}
|
||||
|
@ -372,8 +368,8 @@ module Spectator::DSL
|
|||
begin
|
||||
%mock = {{found_tuple[2].id}}
|
||||
{% for key, value in value_methods %}
|
||||
%stub{key} = ::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock._spectator_define_stub(%stub{key})
|
||||
%stub{key} = ::Mocks::ValueStub.new({{key.id.symbolize}}, {{value}})
|
||||
%mock.__mocks.add_stub(%stub{key})
|
||||
{% end %}
|
||||
::Spectator::Harness.current?.try(&.cleanup { %mock._spectator_reset })
|
||||
%mock
|
||||
|
@ -426,77 +422,46 @@ module Spectator::DSL
|
|||
# This isn't required, but new_mock() should still find this type.
|
||||
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, resolved.name.symbolize} %}
|
||||
|
||||
::Spectator::Mock.inject({{base}}, ::{{resolved.name}}, {{**value_methods}}) {{block}}
|
||||
{% begin %}
|
||||
{{base.id}} {{type.id}}
|
||||
include ::Mocks::Stubbable::Automatic
|
||||
|
||||
{% for key, value in value_methods %}
|
||||
stub_any_args {{key}} = {{value}}
|
||||
{% end %}
|
||||
|
||||
{{block.body if block}}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Targets a stubbable object (such as a mock or double) for operations.
|
||||
# Constructs a stub for a method.
|
||||
#
|
||||
# The *stubbable* must be a `Stubbable` or `StubbedType`.
|
||||
# This method is expected to be followed up with `.to receive()`.
|
||||
# The *method* is the name of the method to stub.
|
||||
#
|
||||
# This is also the start of a fluent interface for defining stubs.
|
||||
#
|
||||
# Allow syntax:
|
||||
# ```
|
||||
# dbl = dbl(:foobar)
|
||||
# allow(dbl).to receive(:foo).and_return(42)
|
||||
# ```
|
||||
def allow(stubbable : Stubbable | StubbedType)
|
||||
::Spectator::Allow.new(stubbable)
|
||||
end
|
||||
|
||||
# Helper method producing a compilation error when attempting to stub a non-stubbable object.
|
||||
#
|
||||
# Triggered in cases like this:
|
||||
# ```
|
||||
# allow(42).to receive(:to_s).and_return("123")
|
||||
# ```
|
||||
def allow(stubbable)
|
||||
{% raise "Target of `allow()` must be stubbable (mock or double)." %}
|
||||
end
|
||||
|
||||
# Begins the creation of a stub.
|
||||
#
|
||||
# The *method* is the name of the method being stubbed.
|
||||
# It should not define any parameters, it should be just the method name as a literal symbol or string.
|
||||
#
|
||||
# Alone, this method returns a `NullStub`, which allows a stubbable object to return nil from a method.
|
||||
# This macro is typically followed up with a method like `and_return` to change the stub's behavior.
|
||||
#
|
||||
# ```
|
||||
# dbl = dbl(:foobar)
|
||||
# allow(dbl).to receive(:foo)
|
||||
# expect(dbl.foo).to be_nil
|
||||
#
|
||||
# allow(dbl).to receive(:foo).and_return(42)
|
||||
# expect(dbl.foo).to eq(42)
|
||||
# ```
|
||||
#
|
||||
# A block can be provided to be run every time the stub is invoked.
|
||||
# The value returned by the block is returned by the stubbed method.
|
||||
#
|
||||
# ```
|
||||
# dbl = dbl(:foobar)
|
||||
# allow(dbl).to receive(:foo) { 42 }
|
||||
# expect(dbl.foo).to eq(42)
|
||||
# allow(dbl).to receive(:some_method)
|
||||
# allow(dbl).to receive(:the_answer).and_return(42)
|
||||
# ```
|
||||
macro receive(method, *, _file = __FILE__, _line = __LINE__, &block)
|
||||
{% if block %}
|
||||
%proc = ->(%args : ::Spectator::AbstractArguments) {
|
||||
{% if !block.args.empty? %}{{*block.args}} = %args {% end %}
|
||||
{{block.body}}
|
||||
}
|
||||
::Spectator::ProcStub.new({{method.id.symbolize}}, %proc, location: ::Spectator::Location.new({{_file}}, {{_line}}))
|
||||
::Mocks::ProcStub.new({{method.id.symbolize}}) {{block}}
|
||||
{% else %}
|
||||
::Spectator::NullStub.new({{method.id.symbolize}}, location: ::Spectator::Location.new({{_file}}, {{_line}}))
|
||||
::Mocks::NilStub.new({{method.id.symbolize}})
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Returns empty arguments.
|
||||
def no_args
|
||||
::Spectator::Arguments.none
|
||||
::Mocks::Arguments.none
|
||||
end
|
||||
|
||||
# Indicates any arguments can be used (no constraint).
|
||||
def any_args
|
||||
::Spectator::Arguments.any
|
||||
::Mocks::Arguments.any
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,12 +11,12 @@ module Spectator
|
|||
end
|
||||
|
||||
# Calls the `error` method on *visitor*.
|
||||
def accept(visitor)
|
||||
def accept(visitor, &)
|
||||
visitor.error(yield self)
|
||||
end
|
||||
|
||||
# One-word description of the result.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "error"
|
||||
end
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ module Spectator
|
|||
# Note: The metadata will not be merged with the parent metadata.
|
||||
def initialize(@context : Context, @entrypoint : self ->,
|
||||
name : String? = nil, location : Location? = nil,
|
||||
@group : ExampleGroup? = nil, metadata = Metadata.new)
|
||||
@group : ExampleGroup? = nil, metadata = nil)
|
||||
super(name, location, metadata)
|
||||
|
||||
# Ensure group is linked.
|
||||
|
@ -58,7 +58,7 @@ module Spectator
|
|||
# Note: The metadata will not be merged with the parent metadata.
|
||||
def initialize(@context : Context, @entrypoint : self ->,
|
||||
@name_proc : Example -> String, location : Location? = nil,
|
||||
@group : ExampleGroup? = nil, metadata = Metadata.new)
|
||||
@group : ExampleGroup? = nil, metadata = nil)
|
||||
super(nil, location, metadata)
|
||||
|
||||
# Ensure group is linked.
|
||||
|
@ -75,7 +75,7 @@ module Spectator
|
|||
# A set of *metadata* can be used for filtering and modifying example behavior.
|
||||
# Note: The metadata will not be merged with the parent metadata.
|
||||
def initialize(name : String? = nil, location : Location? = nil,
|
||||
@group : ExampleGroup? = nil, metadata = Metadata.new, &block : self ->)
|
||||
@group : ExampleGroup? = nil, metadata = nil, &block : self ->)
|
||||
super(name, location, metadata)
|
||||
|
||||
@context = NullContext.new
|
||||
|
@ -93,9 +93,10 @@ module Spectator
|
|||
# A set of *metadata* can be used for filtering and modifying example behavior.
|
||||
# Note: The metadata will not be merged with the parent metadata.
|
||||
def self.pending(name : String? = nil, location : Location? = nil,
|
||||
group : ExampleGroup? = nil, metadata = Metadata.new, reason = nil)
|
||||
group : ExampleGroup? = nil, metadata = nil, reason = nil)
|
||||
# Add pending tag and reason if they don't exist.
|
||||
metadata = metadata.merge({:pending => nil, :reason => reason}) { |_, v, _| v }
|
||||
tags = {:pending => nil, :reason => reason}
|
||||
metadata = metadata ? metadata.merge(tags) { |_, v, _| v } : tags
|
||||
new(name, location, group, metadata) { nil }
|
||||
end
|
||||
|
||||
|
@ -103,8 +104,8 @@ module Spectator
|
|||
# Returns the result of the execution.
|
||||
# The result will also be stored in `#result`.
|
||||
def run : Result
|
||||
Log.debug { "Running example #{self}" }
|
||||
Log.warn { "Example #{self} already ran" } if @finished
|
||||
Log.debug { "Running example: #{self}" }
|
||||
Log.warn { "Example already ran: #{self}" } if @finished
|
||||
|
||||
if pending?
|
||||
Log.debug { "Skipping example #{self} - marked pending" }
|
||||
|
@ -117,7 +118,7 @@ module Spectator
|
|||
|
||||
begin
|
||||
@result = Harness.run do
|
||||
if proc = @name_proc.as?(Proc(Example, String))
|
||||
if proc = @name_proc
|
||||
self.name = proc.call(self)
|
||||
end
|
||||
|
||||
|
@ -142,8 +143,10 @@ module Spectator
|
|||
group.call_before_each(self)
|
||||
group.call_pre_condition(self)
|
||||
end
|
||||
Log.trace { "Running example code for: #{self}" }
|
||||
@entrypoint.call(self)
|
||||
@finished = true
|
||||
Log.trace { "Finished running example code for: #{self}" }
|
||||
if group = @group
|
||||
group.call_post_condition(self)
|
||||
group.call_after_each(self)
|
||||
|
@ -161,7 +164,7 @@ module Spectator
|
|||
# The context casted to an instance of *klass* is provided as a block argument.
|
||||
#
|
||||
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
||||
protected def with_context(klass)
|
||||
protected def with_context(klass, &)
|
||||
context = klass.cast(@context)
|
||||
with context yield
|
||||
end
|
||||
|
@ -181,7 +184,7 @@ module Spectator
|
|||
end
|
||||
|
||||
# Yields this example and all parent groups.
|
||||
def ascend
|
||||
def ascend(&)
|
||||
node = self
|
||||
while node
|
||||
yield node
|
||||
|
@ -191,7 +194,7 @@ module Spectator
|
|||
|
||||
# Constructs the full name or description of the example.
|
||||
# This prepends names of groups this example is part of.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
name = @name
|
||||
|
||||
# Prefix with group's full name if the node belongs to a group.
|
||||
|
@ -210,9 +213,9 @@ module Spectator
|
|||
end
|
||||
|
||||
# Exposes information about the example useful for debugging.
|
||||
def inspect(io)
|
||||
def inspect(io : IO) : Nil
|
||||
super
|
||||
io << ' ' << result
|
||||
io << " - " << result
|
||||
end
|
||||
|
||||
# Creates the JSON representation of the example,
|
||||
|
@ -276,7 +279,7 @@ module Spectator
|
|||
# The block given to this method will be executed within the test context.
|
||||
#
|
||||
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
||||
protected def with_context(klass)
|
||||
protected def with_context(klass, &)
|
||||
context = @example.cast_context(klass)
|
||||
with context yield
|
||||
end
|
||||
|
@ -286,7 +289,7 @@ module Spectator
|
|||
|
||||
# Constructs the full name or description of the example.
|
||||
# This prepends names of groups this example is part of.
|
||||
def to_s(io) : Nil
|
||||
def to_s(io : IO) : Nil
|
||||
@example.to_s(io)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ module Spectator
|
|||
# The *entrypoint* indicates the proc used to invoke the test code in the example.
|
||||
# The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`.
|
||||
def initialize(@context_builder : -> Context, @entrypoint : Example ->,
|
||||
@name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new)
|
||||
@name : String? = nil, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||
end
|
||||
|
||||
# Creates the builder.
|
||||
|
@ -24,7 +24,7 @@ module Spectator
|
|||
# The *name* is an interpolated string that runs in the context of the example.
|
||||
# *location*, and *metadata* will be applied to the `Example` produced by `#build`.
|
||||
def initialize(@context_builder : -> Context, @entrypoint : Example ->,
|
||||
@name : Example -> String, @location : Location? = nil, @metadata : Metadata = Metadata.new)
|
||||
@name : Example -> String, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||
end
|
||||
|
||||
# Constructs an example with previously defined attributes and context.
|
||||
|
|
|
@ -19,14 +19,14 @@ module Spectator
|
|||
protected setter group : ExampleGroup?
|
||||
|
||||
define_hook before_all : ExampleGroupHook do
|
||||
Log.trace { "Processing before_all hooks for #{self}" }
|
||||
Log.trace { "Processing before_all hooks for: #{self}" }
|
||||
|
||||
@group.try &.call_before_all
|
||||
before_all_hooks.each &.call_once
|
||||
end
|
||||
|
||||
define_hook after_all : ExampleGroupHook, :prepend do
|
||||
Log.trace { "Processing after_all hooks for #{self}" }
|
||||
Log.trace { "Processing after_all hooks for: #{self}" }
|
||||
|
||||
after_all_hooks.each &.call_once if finished?
|
||||
if group = @group
|
||||
|
@ -35,21 +35,21 @@ module Spectator
|
|||
end
|
||||
|
||||
define_hook before_each : ExampleHook do |example|
|
||||
Log.trace { "Processing before_each hooks for #{self}" }
|
||||
Log.trace { "Processing before_each hooks for: #{self}" }
|
||||
|
||||
@group.try &.call_before_each(example)
|
||||
before_each_hooks.each &.call(example)
|
||||
end
|
||||
|
||||
define_hook after_each : ExampleHook, :prepend do |example|
|
||||
Log.trace { "Processing after_each hooks for #{self}" }
|
||||
Log.trace { "Processing after_each hooks for: #{self}" }
|
||||
|
||||
after_each_hooks.each &.call(example)
|
||||
@group.try &.call_after_each(example)
|
||||
end
|
||||
|
||||
define_hook around_each : ExampleProcsyHook do |procsy|
|
||||
Log.trace { "Processing around_each hooks for #{self}" }
|
||||
Log.trace { "Processing around_each hooks for: #{self}" }
|
||||
|
||||
around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) }
|
||||
if group = @group
|
||||
|
@ -59,14 +59,14 @@ module Spectator
|
|||
end
|
||||
|
||||
define_hook pre_condition : ExampleHook do |example|
|
||||
Log.trace { "Processing pre_condition hooks for #{self}" }
|
||||
Log.trace { "Processing pre_condition hooks for: #{self}" }
|
||||
|
||||
@group.try &.call_pre_condition(example)
|
||||
pre_condition_hooks.each &.call(example)
|
||||
end
|
||||
|
||||
define_hook post_condition : ExampleHook, :prepend do |example|
|
||||
Log.trace { "Processing post_condition hooks for #{self}" }
|
||||
Log.trace { "Processing post_condition hooks for: #{self}" }
|
||||
|
||||
post_condition_hooks.each &.call(example)
|
||||
@group.try &.call_post_condition(example)
|
||||
|
@ -79,7 +79,7 @@ module Spectator
|
|||
# This group will be assigned to the parent *group* if it is provided.
|
||||
# A set of *metadata* can be used for filtering and modifying example behavior.
|
||||
def initialize(@name : Label = nil, @location : Location? = nil,
|
||||
@group : ExampleGroup? = nil, @metadata : Metadata = Metadata.new)
|
||||
@group : ExampleGroup? = nil, @metadata : Metadata? = nil)
|
||||
# Ensure group is linked.
|
||||
group << self if group
|
||||
end
|
||||
|
@ -87,7 +87,7 @@ module Spectator
|
|||
delegate size, unsafe_fetch, to: @nodes
|
||||
|
||||
# Yields this group and all parent groups.
|
||||
def ascend
|
||||
def ascend(&)
|
||||
group = self
|
||||
while group
|
||||
yield group
|
||||
|
@ -112,11 +112,15 @@ module Spectator
|
|||
|
||||
# Constructs the full name or description of the example group.
|
||||
# This prepends names of groups this group is part of.
|
||||
def to_s(io)
|
||||
# Prefix with group's full name if the node belongs to a group.
|
||||
return unless parent = @group
|
||||
def to_s(io : IO, *, nested = false) : Nil
|
||||
unless parent = @group
|
||||
# Display special string when called directly.
|
||||
io << "<root>" unless nested
|
||||
return
|
||||
end
|
||||
|
||||
parent.to_s(io)
|
||||
# Prefix with group's full name if the node belongs to a group.
|
||||
parent.to_s(io, nested: true)
|
||||
name = @name
|
||||
|
||||
# Add padding between the node names
|
||||
|
@ -126,7 +130,7 @@ module Spectator
|
|||
(parent.name?.is_a?(Symbol) && name.is_a?(String) &&
|
||||
(name.starts_with?('#') || name.starts_with?('.')))
|
||||
|
||||
super
|
||||
super(io)
|
||||
end
|
||||
|
||||
# Adds the specified *node* to the group.
|
||||
|
|
|
@ -28,7 +28,7 @@ module Spectator
|
|||
# Creates the builder.
|
||||
# Initially, the builder will have no children and no hooks.
|
||||
# The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`.
|
||||
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new)
|
||||
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||
end
|
||||
|
||||
# Constructs an example group with previously defined attributes, children, and hooks.
|
||||
|
|
|
@ -42,7 +42,7 @@ module Spectator
|
|||
|
||||
# Produces the string representation of the hook.
|
||||
# Includes the location and label if they're not nil.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "example group hook"
|
||||
|
||||
if (label = @label)
|
||||
|
|
|
@ -18,7 +18,7 @@ module Spectator
|
|||
# This group will be assigned to the parent *group* if it is provided.
|
||||
# A set of *metadata* can be used for filtering and modifying example behavior.
|
||||
def initialize(@item : T, name : Label = nil, location : Location? = nil,
|
||||
group : ExampleGroup? = nil, metadata : Metadata = Metadata.new)
|
||||
group : ExampleGroup? = nil, metadata : Metadata? = nil)
|
||||
super(name, location, group, metadata)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,7 +37,7 @@ module Spectator
|
|||
|
||||
# Produces the string representation of the hook.
|
||||
# Includes the location and label if they're not nil.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "example hook"
|
||||
|
||||
if (label = @label)
|
||||
|
|
|
@ -39,7 +39,7 @@ module Spectator
|
|||
|
||||
# Produces the string representation of the hook.
|
||||
# Includes the location and label if they're not nil.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "example hook"
|
||||
|
||||
if (label = @label)
|
||||
|
|
|
@ -101,8 +101,8 @@ module Spectator
|
|||
|
||||
# Asserts that a method is called some point before the example completes.
|
||||
@[AlwaysInline]
|
||||
def to(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable %}
|
||||
|
||||
to_eventually(stub, message)
|
||||
end
|
||||
|
@ -114,17 +114,32 @@ module Spectator
|
|||
report(match_data, message)
|
||||
end
|
||||
|
||||
# Asserts that some criteria defined by the matcher is satisfied.
|
||||
# Allows a custom message to be used.
|
||||
# Returns the expected value cast as the expected type, if the matcher is satisfied.
|
||||
def to(matcher : Matchers::TypeMatcher(U), message = nil) forall U
|
||||
match_data = matcher.match(@expression)
|
||||
value = @expression.value
|
||||
if report(match_data, message)
|
||||
return value if value.is_a?(U)
|
||||
|
||||
raise "Spectator bug: expected value should have cast to #{U}"
|
||||
else
|
||||
raise TypeCastError.new("#{@expression.label} is expected to be a #{U}, but was actually #{value.class}")
|
||||
end
|
||||
end
|
||||
|
||||
# Asserts that a method is not called before the example completes.
|
||||
@[AlwaysInline]
|
||||
def to_not(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to_not(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable || T < ::Spectator::StubbedType %}
|
||||
|
||||
to_never(stub, message)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def not_to(stub : Stub, message = nil) : Nil
|
||||
def not_to(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
to_not(stub, message)
|
||||
end
|
||||
|
||||
|
@ -136,6 +151,36 @@ module Spectator
|
|||
report(match_data, message)
|
||||
end
|
||||
|
||||
# Asserts that some criteria defined by the matcher is not satisfied.
|
||||
# Allows a custom message to be used.
|
||||
# Returns the expected value cast without the unexpected type, if the matcher is satisfied.
|
||||
def to_not(matcher : Matchers::TypeMatcher(U), message = nil) forall U
|
||||
match_data = matcher.negated_match(@expression)
|
||||
value = @expression.value
|
||||
if report(match_data, message)
|
||||
return value unless value.is_a?(U)
|
||||
|
||||
raise "Spectator bug: expected value should not be #{U}"
|
||||
else
|
||||
raise TypeCastError.new("#{@expression.label} is not expected to be a #{U}, but was actually #{value.class}")
|
||||
end
|
||||
end
|
||||
|
||||
# Asserts that some criteria defined by the matcher is not satisfied.
|
||||
# Allows a custom message to be used.
|
||||
# Returns the expected value cast as a non-nillable type, if the matcher is satisfied.
|
||||
def to_not(matcher : Matchers::NilMatcher, message = nil)
|
||||
match_data = matcher.negated_match(@expression)
|
||||
if report(match_data, message)
|
||||
value = @expression.value
|
||||
return value unless value.nil?
|
||||
|
||||
raise "Spectator bug: expected value should not be nil"
|
||||
else
|
||||
raise NilAssertionError.new("#{@expression.label} is not expected to be nil.")
|
||||
end
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def not_to(matcher, message = nil) : Nil
|
||||
|
@ -143,11 +188,11 @@ module Spectator
|
|||
end
|
||||
|
||||
# Asserts that a method is called some point before the example completes.
|
||||
def to_eventually(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to_eventually(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable || T < ::Spectator::StubbedType %}
|
||||
|
||||
stubbable = @expression.value
|
||||
unless stubbable._spectator_stub_for_method?(stub.method)
|
||||
unless stubbable.__mocks.has_stub?(stub.method_name)
|
||||
# Add stub without an argument constraint.
|
||||
# Avoids confusing logic like this:
|
||||
# ```
|
||||
|
@ -156,13 +201,19 @@ module Spectator
|
|||
# ```
|
||||
# Notice that `#foo` is called, but with different arguments.
|
||||
# Normally this would raise an error, but that should be prevented.
|
||||
unconstrained_stub = stub.with(Arguments.any)
|
||||
stubbable._spectator_define_stub(unconstrained_stub)
|
||||
unconstrained_stub = stub.with(::Mocks::Arguments.any)
|
||||
stubbable.__mocks.add_stub(unconstrained_stub)
|
||||
end
|
||||
|
||||
stubbable._spectator_define_stub(stub)
|
||||
# Apply the stub that is expected to be called.
|
||||
stubbable.__mocks.add_stub(stub)
|
||||
|
||||
# Check if the stub was invoked after the test completes.
|
||||
matcher = Matchers::ReceiveMatcher.new(stub)
|
||||
to_eventually(matcher, message)
|
||||
Harness.current.defer { to(matcher, message) }
|
||||
|
||||
# Prevent leaking stubs between tests.
|
||||
Harness.current.cleanup { stubbable.__mocks.remove_stub(stub) }
|
||||
end
|
||||
|
||||
# Asserts that some criteria defined by the matcher is eventually satisfied.
|
||||
|
@ -173,11 +224,11 @@ module Spectator
|
|||
end
|
||||
|
||||
# Asserts that a method is not called before the example completes.
|
||||
def to_never(stub : Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
||||
def to_never(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
{% raise "The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Mocks::Stubbable %}
|
||||
|
||||
stubbable = @expression.value
|
||||
unless stubbable._spectator_stub_for_method?(stub.method)
|
||||
unless stubbable.__mocks.find_stub(stub.method)
|
||||
# Add stub without an argument constraint.
|
||||
# Avoids confusing logic like this:
|
||||
# ```
|
||||
|
@ -187,17 +238,23 @@ module Spectator
|
|||
# Notice that `#foo` is called, but with different arguments.
|
||||
# Normally this would raise an error, but that should be prevented.
|
||||
unconstrained_stub = stub.with(Arguments.any)
|
||||
stubbable._spectator_define_stub(unconstrained_stub)
|
||||
stubbable.__mocks.add_stub(unconstrained_stub)
|
||||
end
|
||||
|
||||
stubbable._spectator_define_stub(stub)
|
||||
# Apply the stub that could be called in case it is.
|
||||
stubbable.__mocks.add_stub(stub)
|
||||
|
||||
# Check if the stub was invoked after the test completes.
|
||||
matcher = Matchers::ReceiveMatcher.new(stub)
|
||||
to_never(matcher, message)
|
||||
Harness.current.defer { to_not(matcher, message) }
|
||||
|
||||
# Prevent leaking stubs between tests.
|
||||
Harness.current.cleanup { stubbable.__mocks.remove_stub(stub) }
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def never_to(stub : Stub, message = nil) : Nil
|
||||
def never_to(stub : ::Mocks::Stub, message = nil) : Nil
|
||||
to_never(stub, message)
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ module Spectator
|
|||
end
|
||||
|
||||
# Calls the `failure` method on *visitor*.
|
||||
def accept(visitor)
|
||||
def accept(visitor, &)
|
||||
visitor.fail(yield self)
|
||||
end
|
||||
|
||||
|
@ -55,7 +55,7 @@ module Spectator
|
|||
end
|
||||
|
||||
# One-word description of the result.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "fail"
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Increases the indent by the a specific *amount* for the duration of the block.
|
||||
private def indent(amount = INDENT)
|
||||
private def indent(amount = INDENT, &)
|
||||
@indent += amount
|
||||
yield
|
||||
@indent -= amount
|
||||
|
@ -23,7 +23,7 @@ module Spectator::Formatting::Components
|
|||
# The contents of the line should be generated by a block provided to this method.
|
||||
# Ensure that _only_ one line is produced by the block,
|
||||
# otherwise the indent will be lost.
|
||||
private def line(io)
|
||||
private def line(io, &)
|
||||
@indent.times { io << ' ' }
|
||||
yield
|
||||
io.puts
|
||||
|
|
|
@ -16,7 +16,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Writes the comment to the output.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "# " << @content
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,36 +7,38 @@ module Spectator::Formatting::Components
|
|||
# Displays information about an error result.
|
||||
struct ErrorResultBlock < ResultBlock
|
||||
# Creates the component.
|
||||
def initialize(example : Example, index : Int32, @result : ErrorResult, subindex = 0)
|
||||
def initialize(example : Example, index : Int32, @error : Exception, subindex = 0)
|
||||
super(example, index, subindex)
|
||||
end
|
||||
|
||||
# Content displayed on the second line of the block after the label.
|
||||
private def subtitle
|
||||
@result.error.message.try(&.each_line.first)
|
||||
@error.message.try(&.each_line.first)
|
||||
end
|
||||
|
||||
# Prefix for the second line of the block.
|
||||
private def subtitle_label
|
||||
"Error: ".colorize(:red)
|
||||
case @error
|
||||
when ExampleFailed then "Failure: "
|
||||
else "Error: "
|
||||
end.colorize(:red)
|
||||
end
|
||||
|
||||
# Display error information.
|
||||
private def content(io)
|
||||
# Fetch the error and message.
|
||||
error = @result.error
|
||||
lines = error.message.try(&.lines)
|
||||
lines = @error.message.try(&.lines)
|
||||
|
||||
# Write the error and message if available.
|
||||
case
|
||||
when lines.nil? then write_error_class(io, error)
|
||||
when lines.size == 1 then write_error_message(io, error, lines.first)
|
||||
when lines.size > 1 then write_multiline_error_message(io, error, lines)
|
||||
else write_error_class(io, error)
|
||||
when lines.nil? then write_error_class(io)
|
||||
when lines.size == 1 then write_error_message(io, lines.first)
|
||||
when lines.size > 1 then write_multiline_error_message(io, lines)
|
||||
else write_error_class(io)
|
||||
end
|
||||
|
||||
# Display the backtrace if it's available.
|
||||
if backtrace = error.backtrace?
|
||||
if backtrace = @error.backtrace?
|
||||
indent { write_backtrace(io, backtrace) }
|
||||
end
|
||||
|
||||
|
@ -44,24 +46,24 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Display just the error type.
|
||||
private def write_error_class(io, error)
|
||||
private def write_error_class(io)
|
||||
line(io) do
|
||||
io << error.class.colorize(:red)
|
||||
io << @error.class.colorize(:red)
|
||||
end
|
||||
end
|
||||
|
||||
# Display the error type and first line of the message.
|
||||
private def write_error_message(io, error, message)
|
||||
private def write_error_message(io, message)
|
||||
line(io) do
|
||||
io << "#{error.class}: ".colorize(:red)
|
||||
io << "#{@error.class}: ".colorize(:red)
|
||||
io << message
|
||||
end
|
||||
end
|
||||
|
||||
# Display the error type and its multi-line message.
|
||||
private def write_multiline_error_message(io, error, lines)
|
||||
private def write_multiline_error_message(io, lines)
|
||||
# Use the normal formatting for the first line.
|
||||
write_error_message(io, error, lines.first)
|
||||
write_error_message(io, lines.first)
|
||||
|
||||
# Display additional lines after the first.
|
||||
lines.skip(1).each do |entry|
|
||||
|
|
|
@ -9,7 +9,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Produces output for running the previously specified example.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "crystal spec "
|
||||
|
||||
# Use location for argument if it's available, since it's simpler.
|
||||
|
|
|
@ -10,7 +10,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Produces the list of commands to run failed examples.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io.puts "Failed examples:"
|
||||
io.puts
|
||||
@failures.each do |failure|
|
||||
|
|
|
@ -9,7 +9,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Produces the output containing the profiling information.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "Top "
|
||||
io << @profile.size
|
||||
io << " slowest examples ("
|
||||
|
|
|
@ -41,7 +41,7 @@ module Spectator::Formatting::Components
|
|||
private abstract def content(io)
|
||||
|
||||
# Writes the component's output to the specified stream.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
title_line(io)
|
||||
# Ident over to align with the spacing used by the index.
|
||||
indent(index_digit_count + 2) do
|
||||
|
|
|
@ -15,7 +15,7 @@ module Spectator::Formatting::Components
|
|||
# #:##:##
|
||||
# # days #:##:##
|
||||
# ```
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
millis = @span.total_milliseconds
|
||||
return format_micro(io, millis * 1000) if millis < 1
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Displays the stats.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
runtime(io)
|
||||
totals(io)
|
||||
if seed = @report.random_seed?
|
||||
|
|
|
@ -10,7 +10,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Produces the output containing the profiling information.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << "# Top "
|
||||
io << @profile.size
|
||||
io << " slowest examples ("
|
||||
|
|
|
@ -31,7 +31,7 @@ module Spectator::Formatting::Components
|
|||
end
|
||||
|
||||
# Writes the counts to the output.
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << @examples << " examples, " << @failures << " failures"
|
||||
|
||||
if @errors > 0
|
||||
|
|
|
@ -63,15 +63,22 @@ module Spectator::Formatting
|
|||
# Displays one or more blocks for a failed example.
|
||||
# Each block is a failed expectation or error raised in the example.
|
||||
private def dump_failed_example(example, index)
|
||||
result = example.result.as?(ErrorResult)
|
||||
# Retrieve the ultimate reason for failing.
|
||||
error = example.result.as?(FailResult).try(&.error)
|
||||
|
||||
# Prevent displaying duplicated output from expectation.
|
||||
# Display `ExampleFailed` but not `ExpectationFailed`.
|
||||
error = nil if error.responds_to?(:expectation)
|
||||
|
||||
# Gather all failed expectations.
|
||||
failed_expectations = example.result.expectations.select(&.failed?)
|
||||
block_count = failed_expectations.size
|
||||
block_count += 1 if result
|
||||
block_count += 1 if error # Add an extra block for final error if it's significant.
|
||||
|
||||
# Don't use sub-index if there was only one problem.
|
||||
if block_count == 1
|
||||
if result
|
||||
io.puts Components::ErrorResultBlock.new(example, index, result)
|
||||
if error
|
||||
io.puts Components::ErrorResultBlock.new(example, index, error)
|
||||
else
|
||||
io.puts Components::FailResultBlock.new(example, index, failed_expectations.first)
|
||||
end
|
||||
|
@ -79,7 +86,7 @@ module Spectator::Formatting
|
|||
failed_expectations.each_with_index(1) do |expectation, subindex|
|
||||
io.puts Components::FailResultBlock.new(example, index, expectation, subindex)
|
||||
end
|
||||
io.puts Components::ErrorResultBlock.new(example, index, result, block_count) if result
|
||||
io.puts Components::ErrorResultBlock.new(example, index, error, block_count) if error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,7 +43,7 @@ module Spectator
|
|||
# The value of `.current` is set to the harness for the duration of the test.
|
||||
# It will be reset after the test regardless of the outcome.
|
||||
# The result of running the test code will be returned.
|
||||
def self.run : Result
|
||||
def self.run(&) : Result
|
||||
with_harness do |harness|
|
||||
harness.run { yield }
|
||||
end
|
||||
|
@ -53,7 +53,7 @@ module Spectator
|
|||
# The `.current` harness is set to the new harness for the duration of the block.
|
||||
# `.current` is reset to the previous value (probably nil) afterwards, even if the block raises.
|
||||
# The result of the block is returned.
|
||||
private def self.with_harness
|
||||
private def self.with_harness(&)
|
||||
previous = @@current
|
||||
begin
|
||||
@@current = harness = new
|
||||
|
@ -70,7 +70,7 @@ module Spectator
|
|||
|
||||
# Runs test code and produces a result based on the outcome.
|
||||
# The test code should be called from within the block given to this method.
|
||||
def run : Result
|
||||
def run(&) : Result
|
||||
elapsed, error = capture { yield }
|
||||
elapsed2, error2 = capture { run_deferred }
|
||||
run_cleanup
|
||||
|
@ -106,7 +106,7 @@ module Spectator
|
|||
@cleanup << block
|
||||
end
|
||||
|
||||
def aggregate_failures(label = nil)
|
||||
def aggregate_failures(label = nil, &)
|
||||
previous = @aggregate
|
||||
@aggregate = aggregate = [] of Expectation
|
||||
begin
|
||||
|
@ -135,7 +135,7 @@ module Spectator
|
|||
|
||||
# Yields to run the test code and returns information about the outcome.
|
||||
# Returns a tuple with the elapsed time and an error if one occurred (otherwise nil).
|
||||
private def capture : Tuple(Time::Span, Exception?)
|
||||
private def capture(&) : Tuple(Time::Span, Exception?)
|
||||
error = nil
|
||||
elapsed = Time.measure do
|
||||
error = catch { yield }
|
||||
|
@ -146,7 +146,7 @@ module Spectator
|
|||
# Yields to run a block of code and captures exceptions.
|
||||
# If the block of code raises an error, the error is caught and returned.
|
||||
# If the block doesn't raise an error, then nil is returned.
|
||||
private def catch : Exception?
|
||||
private def catch(&) : Exception?
|
||||
yield
|
||||
rescue e
|
||||
e
|
||||
|
|
|
@ -38,7 +38,6 @@ require "./location"
|
|||
require "./location_node_filter"
|
||||
require "./matchers"
|
||||
require "./metadata"
|
||||
require "./mocks"
|
||||
require "./name_node_filter"
|
||||
require "./null_context"
|
||||
require "./null_node_filter"
|
||||
|
|
|
@ -15,7 +15,7 @@ module Spectator
|
|||
# The *collection* is the set of items to create sub-nodes for.
|
||||
# The *iterators* is a list of optional names given to items in the collection.
|
||||
def initialize(@collection : Enumerable(T), name : String? = nil, @iterators : Array(String) = [] of String,
|
||||
location : Location? = nil, metadata : Metadata = Metadata.new)
|
||||
location : Location? = nil, metadata : Metadata? = nil)
|
||||
super(name, location, metadata)
|
||||
end
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ module Spectator
|
|||
# ```text
|
||||
# FILE:LINE
|
||||
# ```
|
||||
def to_s(io)
|
||||
def to_s(io : IO) : Nil
|
||||
io << path << ':' << line
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ module Spectator::Matchers
|
|||
extend self
|
||||
|
||||
# Text displayed when a method is undefined.
|
||||
def inspect(io)
|
||||
def inspect(io : IO) : Nil
|
||||
io << "<Method undefined>"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,7 +97,7 @@ module Spectator::Matchers
|
|||
|
||||
# Runs a block of code and returns the exception it threw.
|
||||
# If no exception was thrown, *nil* is returned.
|
||||
private def capture_exception
|
||||
private def capture_exception(&)
|
||||
exception = nil
|
||||
begin
|
||||
yield
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
require "../mocks/stub"
|
||||
require "../mocks/stubbable"
|
||||
require "../mocks/stubbed_type"
|
||||
require "./matcher"
|
||||
|
||||
module Spectator::Matchers
|
||||
|
@ -9,13 +6,13 @@ module Spectator::Matchers
|
|||
alias Count = Range(Int32?, Int32?)
|
||||
|
||||
# Creates the matcher for expecting a method call matching a stub.
|
||||
def initialize(@stub : Stub, @count : Count = Count.new(1, nil))
|
||||
def initialize(@stub : Mocks::Stub, @count : Count = Count.new(1, nil))
|
||||
end
|
||||
|
||||
# Creates the matcher for expecting a method call with any arguments.
|
||||
# *expected* is an expression evaluating to the method name as a symbol.
|
||||
def initialize(expected : Expression(Symbol))
|
||||
stub = NullStub.new(expected.value).as(Stub)
|
||||
stub = Mocks::NilStub.new(expected.value).as(Mocks::Stub)
|
||||
initialize(stub)
|
||||
end
|
||||
|
||||
|
@ -85,7 +82,7 @@ module Spectator::Matchers
|
|||
end
|
||||
|
||||
# Actually performs the test against the expression (value or block).
|
||||
def match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData
|
||||
def match(actual : Expression(Mocks::Stubbable)) : MatchData
|
||||
stubbed = actual.value
|
||||
calls = relevant_calls(stubbed)
|
||||
if @count.includes?(calls.size)
|
||||
|
@ -102,7 +99,7 @@ module Spectator::Matchers
|
|||
end
|
||||
|
||||
# Performs the test against the expression (value or block), but inverted.
|
||||
def negated_match(actual : Expression(Stubbable) | Expression(StubbedType)) : MatchData
|
||||
def negated_match(actual : Expression(Mocks::Stubbable)) : MatchData
|
||||
stubbed = actual.value
|
||||
calls = relevant_calls(stubbed)
|
||||
if @count.includes?(calls.size)
|
||||
|
@ -135,7 +132,7 @@ module Spectator::Matchers
|
|||
|
||||
# Filtered list of method calls relevant to the matcher.
|
||||
private def relevant_calls(stubbable)
|
||||
stubbable._spectator_calls.select { |call| @stub === call }
|
||||
stubbable.__mocks.calls.select { |call| @stub === call }
|
||||
end
|
||||
|
||||
private def humanize_count
|
||||
|
@ -148,11 +145,11 @@ module Spectator::Matchers
|
|||
|
||||
# Formatted list of method calls.
|
||||
private def method_call_list(stubbable)
|
||||
calls = stubbable._spectator_calls
|
||||
calls = stubbable.__mocks.calls
|
||||
if calls.empty?
|
||||
"None"
|
||||
else
|
||||
calls.join("\n")
|
||||
calls.join('\n')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require "../expression"
|
||||
require "../value"
|
||||
require "./standard_matcher"
|
||||
|
||||
module Spectator::Matchers
|
||||
|
@ -22,7 +24,7 @@ module Spectator::Matchers
|
|||
|
||||
# Creates the value matcher.
|
||||
# The expected value is stored for later use.
|
||||
def initialize(@expected : Value(ExpectedType))
|
||||
def initialize(@expected : ::Spectator::Value(ExpectedType))
|
||||
end
|
||||
|
||||
# Additional information about the match failure.
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "./mocks/*"
|
||||
|
||||
module Spectator
|
||||
# Functionality for mocking existing types.
|
||||
module Mocks
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
module Spectator
|
||||
# Untyped arguments to a method call (message).
|
||||
abstract class AbstractArguments
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
require "./stub"
|
||||
require "./stubbable"
|
||||
require "./stubbed_type"
|
||||
|
||||
module Spectator
|
||||
# Targets a stubbable object.
|
||||
#
|
||||
# This type is effectively part of the mock DSL.
|
||||
# It is primarily used in the mock DSL to provide this syntax:
|
||||
# ```
|
||||
# allow(dbl).to
|
||||
# ```
|
||||
struct Allow(T)
|
||||
# Creates the stub target.
|
||||
#
|
||||
# The *target* must be a kind of `Stubbable` or `StubbedType`.
|
||||
def initialize(@target : T)
|
||||
{% raise "Target of `allow` must be stubbable (a mock or double)." unless T < Stubbable || T < StubbedType %}
|
||||
end
|
||||
|
||||
# Applies a stub to the targeted stubbable object.
|
||||
def to(stub : Stub) : Nil
|
||||
@target._spectator_define_stub(stub)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,91 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
|
||||
module Spectator
|
||||
# Arguments used in a method call.
|
||||
#
|
||||
# Can also be used to match arguments.
|
||||
# *T* must be a `Tuple` type representing the positional arguments.
|
||||
# *NT* must be a `NamedTuple` type representing the keyword arguments.
|
||||
class Arguments(T, NT) < AbstractArguments
|
||||
# Positional arguments.
|
||||
getter args : T
|
||||
|
||||
# Keyword arguments.
|
||||
getter kwargs : NT
|
||||
|
||||
# Creates arguments used in a method call.
|
||||
def initialize(@args : T, @kwargs : NT)
|
||||
end
|
||||
|
||||
# Constructs an instance from literal arguments.
|
||||
def self.capture(*args, **kwargs) : AbstractArguments
|
||||
new(args, kwargs).as(AbstractArguments)
|
||||
end
|
||||
|
||||
# Instance of empty arguments.
|
||||
class_getter none : AbstractArguments = capture
|
||||
|
||||
# Returns unconstrained arguments.
|
||||
def self.any : AbstractArguments?
|
||||
nil.as(AbstractArguments?)
|
||||
end
|
||||
|
||||
# Returns the positional argument at the specified index.
|
||||
def [](index : Int)
|
||||
@args[index]
|
||||
end
|
||||
|
||||
# Returns the specified named argument.
|
||||
def [](arg : Symbol)
|
||||
@kwargs[arg]
|
||||
end
|
||||
|
||||
# Constructs a string representation of the arguments.
|
||||
def to_s(io : IO) : Nil
|
||||
return io << "(no args)" if args.empty? && kwargs.empty?
|
||||
|
||||
io << '('
|
||||
|
||||
# Add the positional arguments.
|
||||
args.each_with_index do |arg, i|
|
||||
io << ", " if i > 0
|
||||
arg.inspect(io)
|
||||
end
|
||||
|
||||
# Add the keyword arguments.
|
||||
size = args.size + kwargs.size
|
||||
kwargs.each_with_index(args.size) do |k, v, i|
|
||||
io << ", " if 0 < i < size
|
||||
io << k << ": "
|
||||
v.inspect(io)
|
||||
end
|
||||
|
||||
io << ')'
|
||||
end
|
||||
|
||||
# Checks if this set of arguments and another are equal.
|
||||
def ==(other : Arguments)
|
||||
args == other.args && kwargs == other.kwargs
|
||||
end
|
||||
|
||||
# Checks if another set of arguments matches this set of arguments.
|
||||
def ===(other : Arguments)
|
||||
args === other.args && named_tuples_match?(kwargs, other.kwargs)
|
||||
end
|
||||
|
||||
# Checks if two named tuples match.
|
||||
#
|
||||
# Uses case equality (`===`) on every key-value pair.
|
||||
# NamedTuple doesn't have a `===` operator, even though Tuple does.
|
||||
private def named_tuples_match?(a : NamedTuple, b : NamedTuple)
|
||||
return false if a.size != b.size
|
||||
|
||||
a.each do |k, v|
|
||||
return false unless b.has_key?(k)
|
||||
return false unless v === b[k]
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,191 +0,0 @@
|
|||
require "./arguments"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./stubbable"
|
||||
require "./stubbed_name"
|
||||
require "./stubbed_type"
|
||||
require "./unexpected_message"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Stands in for an object for testing that a SUT calls expected methods.
|
||||
#
|
||||
# Handles all messages (method calls), but only responds to those configured.
|
||||
# Methods called that were not configured will raise `UnexpectedMessage`.
|
||||
# Doubles should be defined with the `#define` macro.
|
||||
#
|
||||
# Use `#_spectator_define_stub` to override behavior of a method in the double.
|
||||
# Only methods defined in the double's type can have stubs.
|
||||
# New methods are not defines when a stub is added that doesn't have a matching method name.
|
||||
abstract class Double
|
||||
include Stubbable
|
||||
extend StubbedType
|
||||
|
||||
Log = Spectator::Log.for(self)
|
||||
|
||||
# Defines a test double type.
|
||||
#
|
||||
# The *type_name* is the name to give the class.
|
||||
# Instances of the double can be named by providing a *name*.
|
||||
# This can be a symbol, string, or even a type.
|
||||
# See `StubbedName` for details.
|
||||
#
|
||||
# After the names, a collection of key-value pairs can be given to quickly define methods.
|
||||
# Each key is the method name, and the corresponding value is the value returned by the method.
|
||||
# These methods accept any arguments.
|
||||
# Additionally, these methods can be overridden later with stubs.
|
||||
#
|
||||
# Lastly, a block can be provided to define additional methods and stubs.
|
||||
# The block is evaluated in the context of the double's class.
|
||||
#
|
||||
# ```
|
||||
# Double.define(SomeDouble, meth1: 42, meth2: "foobar") do
|
||||
# stub abstract def meth3 : Symbol
|
||||
#
|
||||
# # Default implementation with a dynamic value.
|
||||
# stub def meth4
|
||||
# Time.utc
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
macro define(type_name, name = nil, **value_methods, &block)
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
class {{type_name.id}} < {{@type.name}}
|
||||
{% for key, value in value_methods %}
|
||||
default_stub def {{key.id}}(*%args, **%kwargs)
|
||||
{{value}}
|
||||
end
|
||||
|
||||
default_stub def {{key.id}}(*%args, **%kwargs, &)
|
||||
{{key.id}}
|
||||
end
|
||||
{% end %}
|
||||
|
||||
{% if block %}{{block.body}}{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
@calls = [] of MethodCall
|
||||
|
||||
private class_getter _spectator_stubs : Array(Stub) = [] of Stub
|
||||
|
||||
class_getter _spectator_calls : Array(MethodCall) = [] of MethodCall
|
||||
|
||||
# Creates the double.
|
||||
#
|
||||
# An initial set of *stubs* can be provided.
|
||||
def initialize(@stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub)
|
||||
end
|
||||
|
||||
# Creates the double.
|
||||
#
|
||||
# An initial set of stubs can be provided with *value_methods*.
|
||||
def initialize(**value_methods)
|
||||
@stubs = value_methods.map do |key, value|
|
||||
ValueStub.new(key, value).as(Stub)
|
||||
end
|
||||
end
|
||||
|
||||
# Compares against another object.
|
||||
#
|
||||
# Always returns false.
|
||||
# This method exists as a workaround to provide an alternative to `Object#same?`,
|
||||
# which only accepts a `Reference` or `Nil`.
|
||||
def same?(other) : Bool
|
||||
false
|
||||
end
|
||||
|
||||
# Defines a stub to change the behavior of a method in this double.
|
||||
#
|
||||
# NOTE: Defining a stub for a method not defined in the double's type has no effect.
|
||||
protected def _spectator_define_stub(stub : Stub) : Nil
|
||||
Log.debug { "Defined stub for #{_spectator_stubbed_name} #{stub}" }
|
||||
@stubs.unshift(stub)
|
||||
end
|
||||
|
||||
protected def _spectator_clear_stubs : Nil
|
||||
Log.debug { "Clearing stubs for #{_spectator_stubbed_name}" }
|
||||
@stubs.clear
|
||||
end
|
||||
|
||||
private def _spectator_find_stub(call : MethodCall) : Stub?
|
||||
Log.debug { "Finding stub for #{call}" }
|
||||
stub = @stubs.find &.===(call)
|
||||
Log.debug { stub ? "Found stub #{stub} for #{call}" : "Did not find stub for #{call}" }
|
||||
stub
|
||||
end
|
||||
|
||||
def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
@stubs.any? { |stub| stub.method == method }
|
||||
end
|
||||
|
||||
def _spectator_record_call(call : MethodCall) : Nil
|
||||
@calls << call
|
||||
end
|
||||
|
||||
def _spectator_calls
|
||||
@calls
|
||||
end
|
||||
|
||||
def _spectator_clear_calls : Nil
|
||||
@calls.clear
|
||||
end
|
||||
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
{% if anno = @type.annotation(StubbedName) %}
|
||||
"#<Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
|
||||
{% else %}
|
||||
"#<Double Anonymous>"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def self._spectator_stubbed_name : String
|
||||
{% if anno = @type.annotation(StubbedName) %}
|
||||
"#<Class Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
|
||||
{% else %}
|
||||
"#<Class Double Anonymous>"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def _spectator_stub_fallback(call : MethodCall, &)
|
||||
Log.trace { "Fallback for #{call} - call original" }
|
||||
yield
|
||||
end
|
||||
|
||||
private def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
_spectator_stub_fallback(call) { yield }
|
||||
end
|
||||
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
Log.info do
|
||||
break unless _spectator_stub_for_method?(call.method)
|
||||
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
_spectator_abstract_stub_fallback(call)
|
||||
end
|
||||
|
||||
# "Hide" existing methods and methods from ancestors by overriding them.
|
||||
macro finished
|
||||
stub_type {{@type.name(generic_args: false)}}
|
||||
end
|
||||
|
||||
# Handle all methods but only respond to configured messages.
|
||||
# Raises an `UnexpectedMessage` error for non-configures messages.
|
||||
macro method_missing(call)
|
||||
Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" }
|
||||
args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
|
||||
call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args)
|
||||
_spectator_record_call(call)
|
||||
|
||||
raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,49 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./stub"
|
||||
require "./stub_modifiers"
|
||||
|
||||
module Spectator
|
||||
# Stub that raises an exception.
|
||||
class ExceptionStub < Stub
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : Nil
|
||||
raise @exception
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @exception, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub that raises an exception.
|
||||
def and_raise(exception : Exception)
|
||||
ExceptionStub.new(method, exception, constraint, location)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def and_raise(exception_class : Exception.class, message)
|
||||
exception = exception_class.new(message)
|
||||
and_raise(exception)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def and_raise(message : String? = nil)
|
||||
exception = Exception.new(message)
|
||||
and_raise(exception)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def and_raise(exception_class : Exception.class)
|
||||
exception = exception_class.new
|
||||
and_raise(exception)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,82 +0,0 @@
|
|||
require "../label"
|
||||
require "./arguments"
|
||||
require "./double"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Stands in for an object for testing that a SUT calls expected methods.
|
||||
#
|
||||
# Handles all messages (method calls), but only responds to those configured.
|
||||
# Methods called that were not configured will raise `UnexpectedMessage`.
|
||||
#
|
||||
# Use `#_spectator_define_stub` to override behavior of a method in the double.
|
||||
# Only methods defined in the double's type can have stubs.
|
||||
# New methods are not defines when a stub is added that doesn't have a matching method name.
|
||||
class LazyDouble(Messages) < Double
|
||||
@name : String?
|
||||
|
||||
def initialize(_spectator_double_name = nil, _spectator_double_stubs = [] of Stub, **@messages : **Messages)
|
||||
@name = _spectator_double_name.try &.inspect
|
||||
message_stubs = messages.map do |method, value|
|
||||
ValueStub.new(method, value)
|
||||
end
|
||||
|
||||
super(_spectator_double_stubs + message_stubs)
|
||||
end
|
||||
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
"#<LazyDouble #{@name || "Anonymous"}>"
|
||||
end
|
||||
|
||||
private def _spectator_stub_fallback(call : MethodCall, &)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
else
|
||||
Log.trace { "Fallback for #{call} - call original" }
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Handles all messages.
|
||||
macro method_missing(call)
|
||||
Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" }
|
||||
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
|
||||
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original message.
|
||||
\{% if Messages.keys.includes?({{call.name.symbolize}}) %}
|
||||
_spectator_cast_stub_value(%stub, %call, \{{Messages[{{call.name.symbolize}}.id]}})
|
||||
\{% else %}
|
||||
# A method that was not defined during initialization was stubbed.
|
||||
# Even though all stubs will have a #call method, the compiler doesn't seem to agree.
|
||||
# Assert that it will (this should never fail).
|
||||
raise TypeCastError.new("Stub has no value") unless %stub.responds_to?(:call)
|
||||
|
||||
# Return the value of the stub as-is.
|
||||
# Might want to give a warning here, as this may produce a "bloated" union of all known stub types.
|
||||
%stub.call(%call)
|
||||
\{% end %}
|
||||
else
|
||||
# A stub wasn't found, invoke the fallback logic.
|
||||
\{% if Messages.keys.includes?({{call.name.symbolize}}.id) %}
|
||||
# Pass along the message type and a block to invoke it.
|
||||
_spectator_stub_fallback(%call, \{{Messages[{{call.name.symbolize}}.id]}}) { @messages[{{call.name.symbolize}}] }
|
||||
\{% else %}
|
||||
# Message received for a methods that isn't stubbed nor defined when initialized.
|
||||
_spectator_abstract_stub_fallback(%call)
|
||||
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
|
||||
\{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
require "./arguments"
|
||||
|
||||
module Spectator
|
||||
# Stores information about a call to a method.
|
||||
class MethodCall
|
||||
# Name of the method.
|
||||
getter method : Symbol
|
||||
|
||||
# Arguments passed to the method.
|
||||
getter arguments : AbstractArguments
|
||||
|
||||
# Creates a method call.
|
||||
def initialize(@method : Symbol, @arguments : AbstractArguments = Arguments.none)
|
||||
end
|
||||
|
||||
# Creates a method call by splatting its arguments.
|
||||
def self.capture(method : Symbol, *args, **kwargs)
|
||||
arguments = Arguments.new(args, kwargs).as(AbstractArguments)
|
||||
new(method, arguments)
|
||||
end
|
||||
|
||||
# Constructs a string containing the method name and arguments.
|
||||
def to_s(io : IO) : Nil
|
||||
io << '#' << method << arguments
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,190 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./mocked"
|
||||
require "./reference_mock_registry"
|
||||
require "./stub"
|
||||
require "./stubbed_name"
|
||||
require "./stubbed_type"
|
||||
require "./value_mock_registry"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Module providing macros for defining new mocks from existing types and injecting mock features into concrete types.
|
||||
module Mock
|
||||
# Defines a type that inherits from another, existing type.
|
||||
# The newly defined subtype will have mocking functionality.
|
||||
#
|
||||
# Methods from the inherited type will be overridden to support stubs.
|
||||
# *base* is the keyword for the type being defined - class or struct.
|
||||
# *mocked_type* is the original type to inherit from.
|
||||
# *type_name* is the name of the new type to define.
|
||||
# An optional *name* of the mock can be provided.
|
||||
# Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type.
|
||||
#
|
||||
# A block can be provided to define additional methods and stubs.
|
||||
# The block is evaluated in the context of the derived type.
|
||||
#
|
||||
# ```
|
||||
# Mock.define_subtype(:class, SomeType, meth1: 42, meth2: "foobar") do
|
||||
# stub abstract def meth3 : Symbol
|
||||
#
|
||||
# # Default implementation with a dynamic value.
|
||||
# stub def meth4
|
||||
# Time.utc
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block)
|
||||
{% begin %}
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
|
||||
include ::Spectator::Mocked
|
||||
extend ::Spectator::StubbedType
|
||||
|
||||
{% begin %}
|
||||
private getter(_spectator_stubs) do
|
||||
[
|
||||
{% for key, value in value_methods %}
|
||||
::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}),
|
||||
{% end %}
|
||||
] of ::Spectator::Stub
|
||||
end
|
||||
{% end %}
|
||||
|
||||
def _spectator_clear_stubs : Nil
|
||||
@_spectator_stubs = nil
|
||||
end
|
||||
|
||||
private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub
|
||||
|
||||
class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
|
||||
|
||||
getter _spectator_calls = [] of ::Spectator::MethodCall
|
||||
|
||||
# Returns the mock's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Mock {{mocked_type.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
private def self._spectator_stubbed_name : String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Class Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Class Mock {{mocked_type.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
macro finished
|
||||
stub_type {{mocked_type.id}}
|
||||
|
||||
{% if block %}{{block.body}}{% end %}
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Injects mock functionality into an existing type.
|
||||
#
|
||||
# Generally this method of mocking should be avoiding.
|
||||
# It modifies types being tested, the mock functionality won't exist outside of tests.
|
||||
# This option should only be used when sub-types are not possible (e.g. concrete struct).
|
||||
#
|
||||
# Methods in the type will be overridden to support stubs.
|
||||
# The original method functionality will still be accessible, but pass through mock code first.
|
||||
# *base* is the keyword for the type being defined - class or struct.
|
||||
# *type_name* is the name of the type to inject mock functionality into.
|
||||
# This _must_ be full, resolvable path to the type.
|
||||
# An optional *name* of the mock can be provided.
|
||||
# Any key-value pairs provided with *value_methods* are used as initial stubs for the mocked type.
|
||||
#
|
||||
# A block can be provided to define additional methods and stubs.
|
||||
# The block is evaluated in the context of the derived type.
|
||||
#
|
||||
# ```
|
||||
# Mock.inject(:struct, SomeType, meth1: 42, meth2: "foobar") do
|
||||
# stub abstract def meth3 : Symbol
|
||||
#
|
||||
# # Default implementation with a dynamic value.
|
||||
# stub def meth4
|
||||
# Time.utc
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
macro inject(base, type_name, name = nil, **value_methods, &block)
|
||||
{% begin %}
|
||||
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
|
||||
{{base.id}} ::{{type_name.id}}
|
||||
include ::Spectator::Mocked
|
||||
extend ::Spectator::StubbedType
|
||||
|
||||
{% if base == :class %}
|
||||
@@_spectator_mock_registry = ::Spectator::ReferenceMockRegistry.new
|
||||
{% elsif base == :struct %}
|
||||
@@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new
|
||||
{% else %}
|
||||
{% raise "Unsupported base type #{base} for injecting mock" %}
|
||||
{% end %}
|
||||
|
||||
private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub
|
||||
|
||||
class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
|
||||
|
||||
private def _spectator_stubs
|
||||
entry = @@_spectator_mock_registry.fetch(self) do
|
||||
_spectator_default_stubs
|
||||
end
|
||||
entry.stubs
|
||||
end
|
||||
|
||||
def _spectator_clear_stubs : Nil
|
||||
@@_spectator_mock_registry.delete(self)
|
||||
end
|
||||
|
||||
def _spectator_calls
|
||||
entry = @@_spectator_mock_registry.fetch(self) do
|
||||
_spectator_default_stubs
|
||||
end
|
||||
entry.calls
|
||||
end
|
||||
|
||||
private def _spectator_default_stubs
|
||||
{% begin %}
|
||||
[
|
||||
{% for key, value in value_methods %}
|
||||
::Spectator::ValueStub.new({{key.id.symbolize}}, {{value}}),
|
||||
{% end %}
|
||||
] of ::Spectator::Stub
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Returns the mock's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Mock {{type_name.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
# Returns the mock's name formatted for user output.
|
||||
private def self._spectator_stubbed_name : String
|
||||
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
|
||||
"#<Class Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
|
||||
\{% else %}
|
||||
"#<Class Mock {{type_name.id}}>"
|
||||
\{% end %}
|
||||
end
|
||||
|
||||
macro finished
|
||||
stub_type {{type_name.id}}
|
||||
|
||||
{% if block %}{{block.body}}{% end %}
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Stubs and calls for a mock.
|
||||
private struct MockRegistryEntry
|
||||
# Retrieves all stubs defined for a mock.
|
||||
property stubs = [] of Stub
|
||||
|
||||
# Retrieves all calls to stubbed methods.
|
||||
getter calls = [] of MethodCall
|
||||
end
|
||||
end
|
|
@ -1,123 +0,0 @@
|
|||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./stubbable"
|
||||
require "./unexpected_message"
|
||||
|
||||
module Spectator
|
||||
# Mix-in used for mocked types.
|
||||
#
|
||||
# Bridges functionality between mocks and stubs
|
||||
# Implements the abstracts methods from `Stubbable`.
|
||||
#
|
||||
# Types including this module will need to implement `#_spectator_stubs`.
|
||||
# It should return a mutable list of stubs.
|
||||
# This is used to store the stubs for the mocked type.
|
||||
#
|
||||
# Additionally, the `#_spectator_calls` (getter with no arguments) must be implemented.
|
||||
# It should return a mutable list of method calls.
|
||||
# This is used to store the calls to stubs for the mocked type.
|
||||
module Mocked
|
||||
include Stubbable
|
||||
|
||||
# Retrieves an mutable collection of stubs.
|
||||
abstract def _spectator_stubs
|
||||
|
||||
def _spectator_define_stub(stub : ::Spectator::Stub) : Nil
|
||||
_spectator_stubs.unshift(stub)
|
||||
end
|
||||
|
||||
def _spectator_clear_stubs : Nil
|
||||
_spectator_stubs.clear
|
||||
end
|
||||
|
||||
private def _spectator_find_stub(call : ::Spectator::MethodCall) : ::Spectator::Stub?
|
||||
_spectator_stubs.find &.===(call)
|
||||
end
|
||||
|
||||
def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
_spectator_stubs.any? { |stub| stub.method == method }
|
||||
end
|
||||
|
||||
def _spectator_record_call(call : MethodCall) : Nil
|
||||
_spectator_calls << call
|
||||
end
|
||||
|
||||
def _spectator_calls(method : Symbol) : Enumerable(MethodCall)
|
||||
_spectator_calls.select { |call| call.method == method }
|
||||
end
|
||||
|
||||
def _spectator_clear_calls : Nil
|
||||
_spectator_calls.clear
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_stub_fallback(call : MethodCall, &)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Spectator::Log.info do # FIXME: Don't log to top-level Spectator logger (use mock or double logger).
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
yield # Uninteresting message, allow through.
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
value = _spectator_stub_fallback(call) { yield }
|
||||
|
||||
begin
|
||||
type.cast(value)
|
||||
rescue TypeCastError
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `#{value.inspect}`, but returned type must be `#{type}`.")
|
||||
end
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
Spectator::Log.info do # FIXME: Don't log to top-level Spectator logger (use mock or double logger).
|
||||
break unless _spectator_stub_for_method?(call.method)
|
||||
|
||||
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
|
||||
end
|
||||
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
end
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
value = _spectator_abstract_stub_fallback(call)
|
||||
|
||||
begin
|
||||
type.cast(value)
|
||||
rescue TypeCastError
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `#{value.inspect}`, but returned type must be `#{type}`.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,35 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./stub_modifiers"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that responds with a multiple values in succession.
|
||||
class MultiValueStub(T) < TypedStub(T)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : T
|
||||
if @values.size == 1
|
||||
@values.first
|
||||
else
|
||||
@values.shift
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @values, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @values : Array(T), constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub that returns multiple values in succession.
|
||||
def and_return(value, *values)
|
||||
MultiValueStub.new(method, [value, *values], constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,64 +0,0 @@
|
|||
require "./double"
|
||||
require "./method_call"
|
||||
require "./stubbed_name"
|
||||
require "./unexpected_message"
|
||||
|
||||
module Spectator
|
||||
# Stands in for an object for testing that a SUT calls expected methods.
|
||||
#
|
||||
# Handles all messages (method calls), but only responds to those configured.
|
||||
# Methods called that were not configured will return self.
|
||||
# Doubles should be defined with the `#define` macro.
|
||||
#
|
||||
# Use `#_spectator_define_stub` to override behavior of a method in the double.
|
||||
# Only methods defined in the double's type can have stubs.
|
||||
# New methods are not defines when a stub is added that doesn't have a matching method name.
|
||||
abstract class NullDouble < Double
|
||||
# Returns the double's name formatted for user output.
|
||||
private def _spectator_stubbed_name : String
|
||||
{% if anno = @type.annotation(StubbedName) %}
|
||||
"#<NullDouble " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
|
||||
{% else %}
|
||||
"#<NullDouble Anonymous>"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
else
|
||||
Log.trace { "Fallback for #{call} - return self" }
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
# Specialization that matches when the return type matches self.
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall, _type : self)
|
||||
_spectator_abstract_stub_fallback(call)
|
||||
end
|
||||
|
||||
# Default implementation that raises a `TypeCastError` since the return type isn't self.
|
||||
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
if _spectator_stub_for_method?(call.method)
|
||||
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
|
||||
raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
|
||||
else
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.")
|
||||
end
|
||||
end
|
||||
|
||||
# Handles all undefined messages.
|
||||
# Returns stubbed values if available, otherwise delegates to `#_spectator_abstract_stub_fallback`.
|
||||
macro method_missing(call)
|
||||
Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" }
|
||||
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
|
||||
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
require "./typed_stub"
|
||||
require "./value_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that does nothing and returns nil.
|
||||
class NullStub < TypedStub(Nil)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : Nil
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
require "../location"
|
||||
require "./arguments"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Stub that responds with a value returned by calling a proc.
|
||||
class ProcStub(T) < TypedStub(T)
|
||||
# Invokes the stubbed implementation.
|
||||
def call(call : MethodCall) : T
|
||||
@proc.call(call.arguments)
|
||||
end
|
||||
|
||||
# Returns a new stub with constrained arguments.
|
||||
def with_constraint(constraint : AbstractArguments?)
|
||||
self.class.new(method, @proc, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, @proc : Proc(AbstractArguments, T), constraint : AbstractArguments? = nil, location : Location? = nil)
|
||||
super(method, constraint, location)
|
||||
end
|
||||
|
||||
# Creates the stub.
|
||||
def initialize(method : Symbol, constraint : AbstractArguments? = nil, location : Location? = nil, &block : Proc(AbstractArguments, T))
|
||||
initialize(method, block, constraint, location)
|
||||
end
|
||||
end
|
||||
|
||||
module StubModifiers
|
||||
# Returns a new stub with an argument constraint.
|
||||
def with(*args, **kwargs, &block : AbstractArguments -> T) forall T
|
||||
constraint = Arguments.new(args, kwargs).as(AbstractArguments)
|
||||
ProcStub(T).new(method, block, constraint, location)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,47 +0,0 @@
|
|||
require "./mock_registry_entry"
|
||||
require "./stub"
|
||||
|
||||
module Spectator
|
||||
# Stores collections of stubs for mocked reference (class) types.
|
||||
#
|
||||
# This type is intended for all mocked reference types that have functionality "injected."
|
||||
# That is, the type itself has mock functionality bolted on.
|
||||
# Adding instance members should be avoided, for instance, it could mess up serialization.
|
||||
# This registry works around that by mapping mocks (via their memory address) to a collection of stubs.
|
||||
# Doing so prevents adding data to the mocked type.
|
||||
class ReferenceMockRegistry
|
||||
@entries : Hash(Void*, MockRegistryEntry)
|
||||
|
||||
# Creates an empty registry.
|
||||
def initialize
|
||||
@entries = Hash(Void*, MockRegistryEntry).new do |hash, key|
|
||||
hash[key] = MockRegistryEntry.new
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object.
|
||||
def [](object : Reference)
|
||||
key = Box.box(object)
|
||||
@entries[key]
|
||||
end
|
||||
|
||||
# Retrieves all stubs defined for a mocked object.
|
||||
#
|
||||
# Yields to the block on the first retrieval.
|
||||
# This allows a mock to populate the registry with initial stubs.
|
||||
def fetch(object : Reference, & : -> Array(Stub))
|
||||
key = Box.box(object)
|
||||
@entries.fetch(key) do
|
||||
entry = MockRegistryEntry.new
|
||||
entry.stubs = yield
|
||||
@entries[key] = entry
|
||||
end
|
||||
end
|
||||
|
||||
# Clears all stubs defined for a mocked object.
|
||||
def delete(object : Reference) : Nil
|
||||
key = Box.box(object)
|
||||
@entries.delete(key)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
require "./abstract_arguments"
|
||||
require "./arguments"
|
||||
require "./method_call"
|
||||
require "./stub_modifiers"
|
||||
|
||||
module Spectator
|
||||
# Untyped response to a method call (message).
|
||||
abstract class Stub
|
||||
include StubModifiers
|
||||
|
||||
# Name of the method this stub is for.
|
||||
getter method : Symbol
|
||||
|
||||
# Arguments the method must have been called with to provide this response.
|
||||
# Is nil when there's no constraint - only the method name must match.
|
||||
getter constraint : AbstractArguments?
|
||||
|
||||
# Location the stub was defined.
|
||||
getter location : Location?
|
||||
|
||||
# Creates the base of the stub.
|
||||
def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil)
|
||||
end
|
||||
|
||||
# Checks if a method call should receive the response from this stub.
|
||||
def ===(call : MethodCall)
|
||||
return false if method != call.method
|
||||
return true unless constraint = @constraint
|
||||
|
||||
constraint === call.arguments
|
||||
end
|
||||
|
||||
# String representation of the stub, formatted as a method call.
|
||||
def to_s(io : IO) : Nil
|
||||
io << "#" << method << (constraint || "(any args)")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
require "./arguments"
|
||||
|
||||
module Spectator
|
||||
# Mixin intended for `Stub` to return new, modified stubs.
|
||||
module StubModifiers
|
||||
# Returns a new stub of the same type with constrained arguments.
|
||||
abstract def with_constraint(constraint : AbstractArguments?)
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def with(constraint : AbstractArguments?)
|
||||
with_constraint(constraint)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def with(*args, **kwargs)
|
||||
constraint = Arguments.new(args, kwargs).as(AbstractArguments)
|
||||
self.with_constraint(constraint)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,443 +0,0 @@
|
|||
require "../dsl/reserved"
|
||||
require "./arguments"
|
||||
require "./method_call"
|
||||
require "./stub"
|
||||
require "./typed_stub"
|
||||
|
||||
module Spectator
|
||||
# Mix-in for mocks and doubles providing method stubs.
|
||||
#
|
||||
# Macros in this module can override existing methods.
|
||||
# Stubbed methods will look for stubs to evaluate in place of their original functionality.
|
||||
# The primary macro of interest is `#stub`.
|
||||
# The macros are intended to be called from within the type being stubbed.
|
||||
#
|
||||
# Types including this module must define `#_spectator_find_stub` and `#_spectator_stubbed_name`.
|
||||
# These are internal, reserved method names by Spectator, hence the `_spectator` prefix.
|
||||
# These methods can't (and shouldn't) be stubbed.
|
||||
module Stubbable
|
||||
# Attempts to find a stub that satisfies a method call.
|
||||
#
|
||||
# Returns a stub that matches the method *call*
|
||||
# or nil if no stubs satisfy it.
|
||||
abstract def _spectator_find_stub(call : MethodCall) : Stub?
|
||||
|
||||
# Utility method that looks for stubs for methods with the name specified.
|
||||
abstract def _spectator_stub_for_method?(method : Symbol) : Bool
|
||||
|
||||
# Defines a stub to change the behavior of a method.
|
||||
abstract def _spectator_define_stub(stub : Stub) : Nil
|
||||
|
||||
# Clears all previously defined stubs.
|
||||
abstract def _spectator_clear_stubs : Nil
|
||||
|
||||
# Saves a call that was made to a stubbed method.
|
||||
abstract def _spectator_record_call(call : MethodCall) : Nil
|
||||
|
||||
# Retrieves all previously saved calls.
|
||||
abstract def _spectator_calls
|
||||
|
||||
# Clears all previously saved calls.
|
||||
abstract def _spectator_clear_calls : Nil
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_stub_fallback(call : MethodCall, &)
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# Yield to call the original method's implementation.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_stub_fallback(call : MethodCall, type, &)
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_abstract_stub_fallback(call : MethodCall)
|
||||
|
||||
# Method called when a stub isn't found.
|
||||
#
|
||||
# This is similar to `#_spectator_stub_fallback`,
|
||||
# but called when the original (un-stubbed) method isn't available.
|
||||
# The received message is captured in *call*.
|
||||
# The expected return type is provided by *type*.
|
||||
# The stubbed method returns the value returned by this method.
|
||||
# This method can also raise an error if it's impossible to return something.
|
||||
abstract def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||
|
||||
# Utility method returning the stubbed type's name formatted for user output.
|
||||
abstract def _spectator_stubbed_name : String
|
||||
|
||||
# Clears all previously defined calls and stubs.
|
||||
def _spectator_reset : Nil
|
||||
_spectator_clear_calls
|
||||
_spectator_clear_stubs
|
||||
end
|
||||
|
||||
# Redefines a method to accept stubs and provides a default response.
|
||||
#
|
||||
# The *method* must be a `Def`.
|
||||
# That is, a normal looking method definition should follow the `default_stub` keyword.
|
||||
#
|
||||
# ```
|
||||
# default_stub def stubbed_method
|
||||
# "foobar"
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# The method cannot be abstract, as this method requires a default (fallback) response if a stub isn't provided.
|
||||
#
|
||||
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
|
||||
# If no stub is found, then `#_spectator_stub_fallback` is called.
|
||||
# The block provided to `#_spectator_stub_fallback` will invoke the default response.
|
||||
# In other words, `#_spectator_stub_fallback` should yield if it's appropriate to return the default response.
|
||||
private macro default_stub(method)
|
||||
{% if method.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
method = method.exp
|
||||
else
|
||||
raise "`default_stub` requires a method definition"
|
||||
end %}
|
||||
{% raise "Cannot define a stub inside a method" if @def %}
|
||||
{% raise "Default stub cannot be an abstract method" if method.abstract? %}
|
||||
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
|
||||
|
||||
{{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{{method.body}}
|
||||
end
|
||||
|
||||
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
|
||||
|
||||
{% # Reconstruct the method signature.
|
||||
# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of).
|
||||
# This chunk of code must reconstruct the method signature exactly as it was originally.
|
||||
# If it doesn't match, it doesn't override the method and the stubbing won't work.
|
||||
%}
|
||||
{{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}{% end %}
|
||||
)
|
||||
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
# Finding a suitable stub is delegated to the type including the `Stubbable` module.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original method.
|
||||
_spectator_cast_stub_value(%stub, %call, typeof({{original}}),
|
||||
{{ if method.return_type && method.return_type.resolve == NoReturn
|
||||
:no_return
|
||||
elsif method.return_type && method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
|
||||
:nil
|
||||
else
|
||||
:raise
|
||||
end }})
|
||||
else
|
||||
# Delegate missing stub behavior to concrete type.
|
||||
_spectator_stub_fallback(%call, typeof({{original}})) do
|
||||
# Use the default response for the method.
|
||||
{{original}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Redefines a method to require stubs.
|
||||
#
|
||||
# This macro is similar to `#default_stub` but requires that a stub is defined for the method if it's called.
|
||||
#
|
||||
# The *method* should be a `Def`.
|
||||
# That is, a normal looking method definition should follow the `stub` keyword.
|
||||
#
|
||||
# ```
|
||||
# abstract_stub def stubbed_method
|
||||
# "foobar"
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# The method being stubbed doesn't need to exist yet.
|
||||
# Its body of the method passed to this macro is ignored.
|
||||
# The method can be abstract.
|
||||
# It should have a return type annotation, otherwise the compiled return type will probably end up as a giant union.
|
||||
#
|
||||
# ```
|
||||
# abstract_stub abstract def stubbed_method : String
|
||||
# ```
|
||||
#
|
||||
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
|
||||
# If no stub is found, then `#_spectator_stub_fallback` or `#_spectator_abstract_stub_fallback` is called.
|
||||
private macro abstract_stub(method)
|
||||
{% if method.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def)
|
||||
visibility = method.visibility
|
||||
method = method.exp
|
||||
else
|
||||
raise "`abstract_stub` requires a method definition"
|
||||
end %}
|
||||
{% raise "Cannot define a stub inside a method" if @def %}
|
||||
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
|
||||
|
||||
{% # The logic in this macro follows mostly the same logic from `#default_stub`.
|
||||
# The main difference is that this macro cannot access the original method being stubbed.
|
||||
# It might exist or it might not.
|
||||
# The method could also be abstract.
|
||||
# For all intents and purposes, this macro defines logic that doesn't depend on an existing method.
|
||||
%}
|
||||
|
||||
{% unless method.abstract? %}
|
||||
{{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{{method.body}}
|
||||
end
|
||||
|
||||
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
|
||||
{% end %}
|
||||
|
||||
{% # Reconstruct the method signature.
|
||||
# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of).
|
||||
# This chunk of code must reconstruct the method signature exactly as it was originally.
|
||||
# If it doesn't match, it doesn't override the method and the stubbing won't work.
|
||||
%}
|
||||
{{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
|
||||
# Capture information about the call.
|
||||
%args = ::Spectator::Arguments.capture(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}{% end %}
|
||||
)
|
||||
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
|
||||
_spectator_record_call(%call)
|
||||
|
||||
# Attempt to find a stub that satisfies the method call and arguments.
|
||||
# Finding a suitable stub is delegated to the type including the `Stubbable` module.
|
||||
if %stub = _spectator_find_stub(%call)
|
||||
# Cast the stub or return value to the expected type.
|
||||
# This is necessary to match the expected return type of the original method.
|
||||
{% if method.return_type %}
|
||||
# Return type restriction takes priority since it can be a superset of the original implementation.
|
||||
_spectator_cast_stub_value(%stub, %call, {{method.return_type}},
|
||||
{{ if method.return_type.resolve == NoReturn
|
||||
:no_return
|
||||
elsif method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
|
||||
:nil
|
||||
else
|
||||
:raise
|
||||
end }})
|
||||
{% elsif !method.abstract? %}
|
||||
# The method isn't abstract, infer the type it returns without calling it.
|
||||
_spectator_cast_stub_value(%stub, %call, typeof({{original}}))
|
||||
{% else %}
|
||||
# Stubbed method is abstract and there's no return type annotation.
|
||||
# The value of the stub could be returned as-is.
|
||||
# This may produce a "bloated" union of all known stub types,
|
||||
# and generally causes more annoying problems.
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{%call} but cannot resolve the return type. Please add a return type restriction.")
|
||||
{% end %}
|
||||
else
|
||||
# A stub wasn't found, invoke the type-specific fallback logic.
|
||||
{% if method.return_type %}
|
||||
# Pass along just the return type annotation.
|
||||
_spectator_abstract_stub_fallback(%call, {{method.return_type}})
|
||||
{% elsif !method.abstract? %}
|
||||
_spectator_abstract_stub_fallback(%call, typeof({{original}}))
|
||||
{% else %}
|
||||
# Stubbed method is abstract and there's no type annotation.
|
||||
_spectator_abstract_stub_fallback(%call)
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Redefines a method to require stubs.
|
||||
#
|
||||
# The *method* can be a `Def`.
|
||||
# That is, a normal looking method definition should follow the `stub` keyword.
|
||||
#
|
||||
# ```
|
||||
# stub def stubbed_method
|
||||
# "foobar"
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# If the *method* is abstract, then a stub must be provided otherwise attempts to call the method will raise `UnexpectedMessage`.
|
||||
#
|
||||
# ```
|
||||
# stub abstract def stubbed_method
|
||||
# ```
|
||||
#
|
||||
# A `Call` can also be specified.
|
||||
# In this case all methods in the stubbed type and its ancestors that match the call's signature are stubbed.
|
||||
#
|
||||
# ```
|
||||
# stub stubbed_method(arg)
|
||||
# ```
|
||||
#
|
||||
# The method being stubbed doesn't need to exist yet.
|
||||
# Stubbed methods will call `#_spectator_find_stub` with the method call information.
|
||||
# If no stub is found, then `#_spectator_stub_fallback` or `#_spectator_abstract_stub_fallback` is called.
|
||||
macro stub(method)
|
||||
{% raise "Cannot define a stub inside a method" if @def %}
|
||||
|
||||
{% if method.is_a?(Def) %}
|
||||
{% if method.abstract? %}abstract_stub{% else %}default_stub{% end %} {{method}}
|
||||
{% elsif method.is_a?(VisibilityModifier) && method.exp.is_a?(Def) %}
|
||||
{% if method.exp.abstract? %}abstract_stub{% else %}default_stub{% end %} {{method}}
|
||||
{% elsif method.is_a?(Call) %}
|
||||
{% raise "Stub on `Call` unsupported." %}
|
||||
{% else %}
|
||||
{% raise "Unrecognized syntax for `stub` - #{method}" %}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Redefines all methods and ones inherited from its parents and mixins to support stubs.
|
||||
private macro stub_type(type_name = @type)
|
||||
{% type = type_name.resolve
|
||||
# Reverse order of ancestors (there's currently no reverse method for ArrayLiteral).
|
||||
count = type.ancestors.size
|
||||
ancestors = type.ancestors.map_with_index { |_, i| type.ancestors[count - i - 1] } %}
|
||||
{% for ancestor in ancestors %}
|
||||
{% for method in ancestor.methods.reject do |meth|
|
||||
meth.name.starts_with?("_spectator") ||
|
||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
||||
end %}
|
||||
{{(method.abstract? ? :abstract_stub : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
super{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
|
||||
end
|
||||
{% end %}
|
||||
|
||||
{% for method in ancestor.class.methods.reject do |meth|
|
||||
meth.name.starts_with?("_spectator") ||
|
||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
||||
end %}
|
||||
default_stub {{method.visibility.id if method.visibility != :public}} def self.{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
super{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
|
||||
end
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% for method in type.methods.reject do |meth|
|
||||
meth.name.starts_with?("_spectator") ||
|
||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
||||
end %}
|
||||
{{(method.abstract? ? :"abstract_stub abstract" : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{% unless method.abstract? %}
|
||||
{% if type == @type %}previous_def{% else %}super{% end %}{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
|
||||
end
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% for method in type.class.methods.reject do |meth|
|
||||
meth.name.starts_with?("_spectator") ||
|
||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
||||
end %}
|
||||
default_stub {{method.visibility.id if method.visibility != :public}} def self.{{method.name}}(
|
||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
||||
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
|
||||
{% if type == @type %}previous_def{% else %}super{% end %}{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
# Utility macro for casting a stub (and it's return value) to the correct type.
|
||||
#
|
||||
# *stub* is the variable holding the stub.
|
||||
# *call* is the variable holding the captured method call.
|
||||
# *type* is the expected type to cast the value to.
|
||||
# *fail_cast* indicates the behavior used when the value returned by the stub can't be cast to *type*.
|
||||
# - `:nil` - return nil.
|
||||
# - `:raise` - raise a `TypeCastError`.
|
||||
# - `:no_return` - raise as no value should be returned.
|
||||
private macro _spectator_cast_stub_value(stub, call, type, fail_cast = :nil)
|
||||
# Attempt to cast the stub to the method's return type.
|
||||
# If successful, return the value of the stub.
|
||||
# This is a common usage where the return type is simple and matches the stub type exactly.
|
||||
if %typed = {{stub}}.as?(::Spectator::TypedStub({{type}}))
|
||||
%typed.call({{call}})
|
||||
else
|
||||
# The stub couldn't be easily cast to match the return type.
|
||||
|
||||
# Even though all stubs will have a `#call` method, the compiler doesn't seem to agree.
|
||||
# Assert that it will (this should never fail).
|
||||
raise TypeCastError.new("Stub has no value") unless {{stub}}.responds_to?(:call)
|
||||
|
||||
{% if fail_cast == :no_return %}
|
||||
{{stub}}.call({{call}})
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).")
|
||||
{% else %}
|
||||
# Get the value as-is from the stub.
|
||||
# This will be compiled as a union of all known stubbed value types.
|
||||
%value = {{stub}}.call({{call}})
|
||||
|
||||
# Attempt to cast the value to the method's return type.
|
||||
# If successful, which it will be in most cases, return it.
|
||||
# The caller will receive a properly typed value without unions or other side-effects.
|
||||
if %cast = %value.as?({{type}})
|
||||
%cast
|
||||
else
|
||||
{% if fail_cast == :nil %}
|
||||
nil
|
||||
{% elsif fail_cast == :raise %}
|
||||
# The stubbed value was something else entirely and cannot be cast to the return type.
|
||||
# There's something weird going on (compiler bug?) that sometimes causes this class lookup to fail.
|
||||
%type = begin
|
||||
%value.class.to_s
|
||||
rescue
|
||||
"<Unknown>"
|
||||
end
|
||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%type}`, but returned type must be `#{ {{type}} }`.")
|
||||
{% else %}
|
||||
{% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %}
|
||||
{% end %}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue