Compare commits

...

105 Commits

Author SHA1 Message Date
Michael Miller 287758e6af
Update README to point at v0.12.0 2024-02-03 08:05:02 -07:00
Michael Miller f39ceb8eba
Release v0.12.0 2024-02-03 07:56:57 -07:00
Michael Miller 9b1d400ee1
Update CHANGELOG 2024-01-27 11:29:11 -07:00
Michael Miller edb20e5b2f
Additional handling when comparing ranges against unexpected types 2024-01-27 11:25:59 -07:00
Michael Miller 526a998e41
Shorten compare_values case statements 2024-01-27 11:25:25 -07:00
Michael Miller 556d4783bf
Support case equality of tuples, arrays, named tuples, and hashes in stub argument matching 2024-01-27 11:18:10 -07:00
Michael Miller b5fbc96195
Allow matchers to be used in case equality 2024-01-27 11:17:19 -07:00
Michael Miller 5520999b6d
Add spec for GitHub issue 55
https://github.com/icy-arctic-fox/spectator/issues/55
2024-01-27 11:16:57 -07:00
Michael Miller 4a630b1ebf
Bump version to v0.11.7 2023-10-16 17:34:49 -06:00
Michael Miller d72895fe10
Merge branch 'stufro-fix-readme-mocking-example' 2023-05-21 09:58:19 -06:00
Stuart Frost 04f151fddf Fix mocking example in README.md 2023-05-19 19:39:22 +01:00
Michael Miller 9cbb5d2cf7
Workaround issue using Box with union
Addresses issue found relating to https://gitlab.com/arctic-fox/spectator/-/issues/81
See https://github.com/crystal-lang/crystal/issues/11839
2023-03-27 18:37:50 -06:00
Mike Miller 3852606b28 Merge branch 'gh-49' into 'master'
Fix splat argument expansion in method redefinition

See merge request arctic-fox/spectator!36
2023-01-27 00:28:42 +00:00
Michael Miller 726a2e1515
Add non-captured block argument
Preparing for Crystal 1.8.0
https://github.com/crystal-lang/crystal/issues/8764
2023-01-26 17:19:31 -07:00
Michael Miller 5c08427ca0
Add utility script to run nightly spec 2023-01-26 16:43:19 -07:00
Michael Miller 735122a94b
Bump v0.11.6 2023-01-26 16:21:33 -07:00
Michael Miller 9ea5c261b1
Add entry for GitHub issue 49
https://github.com/icy-arctic-fox/spectator/issues/49
2023-01-26 16:19:55 -07:00
Michael Miller 24a860ea11
Add reference to new issue
https://github.com/icy-arctic-fox/spectator/issues/51
2023-01-26 16:18:26 -07:00
Michael Miller 528ad7257d
Disable GitHub issue 49 spec for now 2023-01-26 16:17:29 -07:00
Michael Miller 7149ef7df5
Revert "Compiler bug when using unsafe_as"
This reverts commit cb89589155.
2023-01-26 16:12:54 -07:00
Michael Miller cb89589155
Compiler bug when using unsafe_as 2023-01-25 16:09:16 -07:00
Michael Miller a5e8f11e11
Store type to reduce a bit of bloat 2023-01-23 16:02:30 -07:00
Michael Miller abbd6ffd71
Fix splat argument expansion in method redefinition
The constructed previous_def call was malformed for stub methods.
Resolves the original issue in
https://github.com/icy-arctic-fox/spectator/issues/49
2023-01-23 11:55:52 -07:00
Michael Miller fd372226ab
Revert "Use separate context for example name interpolation"
This reverts commit d46698d81a.
2022-12-21 18:51:09 -07:00
Michael Miller 6a5e5b8f7a
Catch errors while evaluating node labels 2022-12-20 21:40:47 -07:00
Michael Miller 4a0bfc1cb2
Add smoke tag 2022-12-20 20:52:01 -07:00
Michael Miller d46698d81a
Use separate context for example name interpolation
This simplifies some code.
2022-12-20 20:43:47 -07:00
Michael Miller 8c3900adcb
Add support for interpolation in context names 2022-12-20 20:32:40 -07:00
Michael Miller 30602663fe
Add tests for interpolated labels
The context label test intentionally fails.
This functionality still needs to be implemented.
2022-12-20 20:12:58 -07:00
Michael Miller b8901f522a
Remove unnecessary cast 2022-12-20 20:11:09 -07:00
Michael Miller c4bcf54b98
Support casting types with should statements 2022-12-19 22:40:55 -07:00
Michael Miller acf810553a
Use location of the 'should' keyword for their expectation 2022-12-19 22:27:58 -07:00
Michael Miller faff2933e6
Only capture splat if it has a name 2022-12-19 22:15:53 -07:00
Michael Miller 0f8c46d6ef
Support casting types with expect statements 2022-12-19 21:29:21 -07:00
Michael Miller 7620f58fb8
Test file, please ignore 2022-12-19 02:31:12 -07:00
Michael Miller feaf1c6015
Bump version to 0.11.5 2022-12-18 19:15:25 -07:00
Michael Miller 8f80b10fc1
Support injecting mock functionality into modules
Add mock registry for a single module.
2022-12-18 19:04:50 -07:00
Michael Miller a3c55dfa47
Add tests for module mocks docs 2022-12-18 18:52:08 -07:00
Michael Miller fa99987780
Support creating instances of mocked modules via class
This is a bit of a hack.
The `.new` method is added to the module, which creates an instance that includes the mocked module.
No changes to the def_mock and new_mock methods are necessary.

For some reason, infinite recursion occurs when calling `.new` on the class.
To get around the issue for now, the internal method of allocation is used.
That is, allocate + initialize.
2022-12-18 16:04:49 -07:00
Michael Miller d378583054
Support mocking modules 2022-12-18 15:18:20 -07:00
Michael Miller 6255cc85c4
Handle original call reaching to another type
Primary use case for this is mock modules.
Allows default stubs to access more than previous_def and super.
2022-12-18 15:17:48 -07:00
Michael Miller e6584c9f04
Prevent comparing range arguments with non-compatible types in stubs
Addresses https://github.com/icy-arctic-fox/spectator/issues/48
2022-12-18 11:35:43 -07:00
Michael Miller f55c60e01f
Fix README spec
Mocked types cannot be private.
Moved to a module to prevent polluting the global namespace.
2022-12-17 21:01:22 -07:00
Michael Miller 4b68b8e3de
Fix resolution issue when mocked types use custom types
GitLab issue 51 is affected.
https://gitlab.com/arctic-fox/spectator/-/issues/51
Private types cannot be referenced with mocks.
2022-12-17 20:56:16 -07:00
Michael Miller c3e7edc700
Use absolute names of types in mocked type methods
Prevent possibly type name collisions.
This could happen if, for instance, Array or String was redefined in the scope of the mocked type.
2022-12-17 20:37:27 -07:00
Michael Miller 149c0e6e4b
Don't use case-matching for proc arguments
A proc on the left side of === calls itself passing in the right side.
This causes typing issues and is easier to avoid for now.
Procs arguments are compared with standard equality (==) instead of case-equality (===).
2022-12-17 19:19:33 -07:00
Michael Miller 9f54a9e542
Additional handling for passing blocks 2022-12-17 19:16:38 -07:00
Michael Miller 65a4b8e756
Populate previous_def/super with captured block args
The previous_def and super keywords do not propagate blocks.
See: https://github.com/crystal-lang/crystal/issues/10399
This works around the issue by populating arguments if the method uses a block.
2022-12-17 16:41:22 -07:00
Michael Miller b52593dbde
Cleanup 2022-12-17 16:39:47 -07:00
Michael Miller 7e2ec4ee37
Fix 0.11.4 in changelog 2022-12-13 22:59:42 -07:00
Michael Miller 952e949307
Handle 'self' and some other variants in method return types 2022-12-13 22:48:21 -07:00
Michael Miller 293faccd5c
Support free variables in mocked types 2022-12-13 18:22:22 -07:00
Michael Miller 2985ef5919
Remove error handling around type resolution failure
This might not be necessary anymore.
2022-12-09 02:22:21 -07:00
Michael Miller bd44b5562e
Possible fix for GitLab issue 80
Remove `is_a?` check on line 425.
Replace with alternate logic that achieves the same thing.
The `{{type}}` in `is_a?` was causing a compiler bug.
I'm unsure of the root cause, but this works around it.
2022-12-09 02:16:16 -07:00
Michael Miller 47a62ece78
Add reduced test code for GitLab issue 80
https://gitlab.com/arctic-fox/spectator/-/issues/80
Note: This test only triggers a compiler bug when the file is compiled by itself.
Compiling/running the entire spec suite *does not* cause the bug.
2022-12-08 17:14:09 -07:00
Michael Miller 7ffa63718b
Use original type in redefinition comment 2022-12-08 16:55:27 -07:00
Michael Miller 275b217c6c
Allow metadata to be stored as nil 2022-11-29 23:22:42 -07:00
Michael Miller fbe877690d
Adjust call argument matching
Reenable test for https://github.com/icy-arctic-fox/spectator/issues/44 and https://github.com/icy-arctic-fox/spectator/issues/47
2022-11-29 22:31:22 -07:00
Michael Miller a967dce241
Adjust double string representation
to_s and inspect (with variants) are no longer "original implementation."
2022-11-29 21:24:31 -07:00
Michael Miller 1f98bf9ff1
Update CHANGELOG 2022-11-29 20:32:45 -07:00
Michael Miller 5f499336ac
Remove individual spec runs from CI 2022-11-29 20:30:42 -07:00
Michael Miller df10c8e75b
Prevent multiple redefinitions of the same method 2022-11-29 20:29:36 -07:00
Michael Miller a585ef0996
Simplify string (inspect) representation
These types make heavy use of generics and combined types.
Instantiating string representation methods for all possibilities is unecesssary and slows down compilation.
2022-11-29 20:28:15 -07:00
Michael Miller 2d6c8844d4
Remove `time` 2022-11-29 03:34:26 -07:00
Michael Miller 321c15407d
Add utility to test specs individually 2022-11-29 03:14:24 -07:00
Michael Miller c256ef763e
Bump version to 0.11.4 2022-11-27 22:27:52 -07:00
Michael Miller 8efd38fbdd
Split Arguments class by functionality
Code changes for https://github.com/icy-arctic-fox/spectator/issues/47 caused a drastic increase in compilation times.
This improves compilation times by splitting concerns for arguments.
In one case, arguments are used for matching.
In the other, arguments are captured for comparison.
The second case has been moved to a FormalArguments class.
Theoretically, this reduces the complexity and combinations the compiler might be iterating.
2022-11-27 22:26:19 -07:00
Michael Miller 015d36ea4c
Work around strange cast/type checking issue
For some reason, line 421 (the responds to call check) excluded the stub's call type.
Luckily this line doesn't seem to be necessary anymore.
Removed the unecessary quick check.

The tests from spec/spectator/mocks/double_spec:88-96 were failing when they're the only tests in the file.
The non-matching stub wouldn't raise.
Stepping through, attempting to access the value would segfault.
This is because it accessed a stub with String instead of its real Int32 type.
Removing the aforementioned check fixes this.
2022-11-27 19:43:03 -07:00
Michael Miller 318e4eba89
Use shorter string when stub is treated as a message 2022-11-04 22:55:12 -06:00
Michael Miller e2cdc9e08e
Re-enable logger after catching exit
The logger is closed during at-exit hooks that get invoked by Kernel's exit method.
2022-11-04 22:10:59 -06:00
Michael Miller 60b5f151f1
Minor improvements to log output 2022-11-04 22:05:27 -06:00
Michael Miller 8b12262c62
Display <root> when to_s is called directly on the root group 2022-11-04 21:01:32 -06:00
Michael Miller 6e7d215f69
Add type annotations to to_s and inspect 2022-11-04 20:56:02 -06:00
Michael Miller 12eb2e9357
Avoid printing double contents from to_s 2022-11-04 20:35:43 -06:00
Michael Miller 1093571fbd
Add more info to stub.to_s 2022-11-04 20:34:52 -06:00
Michael Miller c00d2fe4e6
Update changelog 2022-11-04 16:57:06 -06:00
Michael Miller a6149b2671
Use `before` instead of `before_each` (same for after) 2022-11-04 16:56:03 -06:00
Michael Miller 4906dfae0d
Add short before/after hook name 2022-11-04 16:55:31 -06:00
Michael Miller 24fd7d1e91
Update Ameba 2022-10-28 18:14:53 -06:00
Michael Miller baff1de1d8
Update changelog
Implemented https://github.com/icy-arctic-fox/spectator/issues/46
2022-10-23 22:37:41 -06:00
Michael Miller 4dacaab6dc
Fix missing keyword arguments after splat 2022-10-23 22:36:20 -06:00
Michael Miller a31ffe3fa3
Fix argument capture
Fix issue added by 8959d28b38
2022-10-23 22:04:28 -06:00
Michael Miller c77da67341
Hide splat label in certain situations
Undefined double methods were reporting splat arguments, which is technically correct.
But for output in these cases, it makes more sense to show the exact calling args.
2022-10-23 21:56:37 -06:00
Michael Miller 8959d28b38
Cleaner call capture and logging for missing methods in doubles 2022-10-23 21:54:12 -06:00
Michael Miller 39e4f8e37a
Use `build` instead of `capture` for `none` 2022-10-23 21:53:24 -06:00
Michael Miller e2130d12d3
Implement arguments case equality
Implements https://github.com/icy-arctic-fox/spectator/issues/47
Some specs are failing and need to be resolved before the new feature is considered done.
2022-10-23 20:42:08 -06:00
Michael Miller 0177a678f9
Avoid shadowing variable 2022-10-23 20:40:56 -06:00
Michael Miller a728a037d4
Rename attributes 2022-10-23 15:37:55 -06:00
Michael Miller 163f94287e
Fix Arguments to_s 2022-10-23 15:27:39 -06:00
Michael Miller e38e3ecc32
Initial rework of arguments to support named positionals 2022-10-23 15:22:50 -06:00
Michael Miller 70d0009db5
Disable issue 47 test for now 2022-10-09 18:23:39 -06:00
Michael Miller d9082dab45
Test behavior and for leakages with allow syntax 2022-10-09 17:14:20 -06:00
Michael Miller b3aa2d62c0
Ensure stubs don't leak between examples 2022-10-09 16:59:39 -06:00
Michael Miller c6afa0adb3
Use different value than original 2022-10-09 16:58:56 -06:00
Michael Miller bc0a9c03c9
Remove runtime compilation tests
These may be readded later.
Right now they're failing because the GitHub issue 44 spec changes the behavior of Process.run.
The changes made by that spec shouldn't leak, but to fix correctly requires substantial changes.
These runtime tests provide little value right now and slow down testing.
2022-10-09 16:47:54 -06:00
Michael Miller 11e227b29f
Simplify method receiver conditional 2022-10-09 16:24:28 -06:00
Michael Miller 8e83edcc35
Simpler conditional block inclusion 2022-10-09 16:04:07 -06:00
Michael Miller 090c95b162
Ensure stubs defined with allow syntax are cleared 2022-10-09 15:48:00 -06:00
Michael Miller 2516803b0d
Add spec for GitHub issue 47
https://github.com/icy-arctic-fox/spectator/issues/47
2022-10-09 15:35:22 -06:00
Michael Miller e9d3f31ac3
Use harness' cleanup instead of defer 2022-10-09 15:32:32 -06:00
Michael Miller 5c910e5a85
Clear stubs defined with `expect().to receive()` syntax after test finishes 2022-10-09 13:57:28 -06:00
Michael Miller 25b9931002
Add ability to remove specific stubs 2022-10-09 13:38:29 -06:00
Michael Miller 422b0efa59
Update test to account for fix in Crystal 1.6
Keyword arguments cannot be used as a short-hand for positional arguments (yet).
https://github.com/icy-arctic-fox/spectator/issues/44
2022-10-09 12:33:31 -06:00
Michael Miller c1e1666449
Formatting 2022-10-08 14:05:53 -06:00
Michael Miller 4dfa5ccb6e
Prevent defining stubs on undefined methods in LazyDouble
In Crystal 1.6, a segfault would occur in the spec spec/spectator/mocks/lazy_double_spec.cr:238
I suspect this is a Crystal bug of some kind, but can't reduce it.
The methods produced by `method_missing` don't have a return type including Symbol.
Symbol is excluded from the union of return types (Int32 | String | Nil).
The program segfaults when calling a method on the actual value, which is a symbol.
It ultimately crashes when producing a failure message, which indicates the value it tested doesn't equal the expected value (a symbol of the same value).
Avoid this issue by preventing stubs on undefined/untyped methods.
2022-10-08 14:04:02 -06:00
106 changed files with 3177 additions and 824 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@
# Ignore JUnit output
output.xml
/test.cr

View File

@ -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:

View File

@ -4,6 +4,61 @@ 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).
## [0.12.0] - 2024-02-03
### Added
- Added ability to use matchers for case equality. [#55](https://github.com/icy-arctic-fox/spectator/issues/55)
- Added support for nested case equality when checking arguments with Array, Tuple, Hash, and NamedTuple.
### Fixed
- Fixed some issues with the `be_within` matcher when used with expected and union types.
## [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)
@ -403,7 +458,12 @@ 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.11.3...master
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.0...master
[0.12.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...v0.12.0
[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

View File

@ -25,7 +25,7 @@ Add this to your application's `shard.yml`:
development_dependencies:
spectator:
gitlab: arctic-fox/spectator
version: ~> 0.11.0
version: ~> 0.12.0
```
Usage
@ -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
```

View File

@ -1,16 +1,16 @@
name: spectator
version: 0.11.3
version: 0.12.0
description: |
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
license: MIT
development_dependencies:
ameba:
github: crystal-ameba/ameba
version: ~> 1.0.0
version: ~> 1.2.0

View File

@ -75,8 +75,8 @@ Spectator.describe "Custom Matchers Docs" do
end
macro be_odd
OddMatcher.new
end
OddMatcher.new
end
specify do
expect(9).to be_odd

View File

@ -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

View File

@ -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")

View 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

View 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
View File

View 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

View File

@ -1,5 +0,0 @@
require "<%= spec_helper_path %>"
Spectator.describe "<%= @example_id %>" do
<%= @example_code %>
end

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View File

@ -0,0 +1,48 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #55" do
GROUP_NAME = "CallCenter"
let(name) { "TimeTravel" }
let(source) { "my.time.travel.experiment" }
class Analytics(T)
property start_time = Time.local
property end_time = Time.local
def initialize(@brain_talker : T)
end
def instrument(*, name, source, &)
@brain_talker.send(payload: {
:group => GROUP_NAME,
:name => name,
:source => source,
:start => start_time,
:end => end_time,
}, action: "analytics")
end
end
double(:brain_talker, send: nil)
let(brain_talker) { double(:brain_talker) }
let(analytics) { Analytics.new(brain_talker) }
it "tracks the time it takes to run the block" do
analytics.start_time = expected_start_time = Time.local
expected_end_time = expected_start_time + 10.seconds
analytics.end_time = expected_end_time + 0.5.seconds # Offset to ensure non-exact match.
analytics.instrument(name: name, source: source) do
end
expect(brain_talker).to have_received(:send).with(payload: {
:group => GROUP_NAME,
:name => name,
:source => source,
:start => expected_start_time,
:end => be_within(1.second).of(expected_end_time),
}, action: "analytics")
end
end

View File

@ -1,31 +1,33 @@
require "../spec_helper"
private class Foo
def call(str : String) : String?
""
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
def alt1_call(str : String) : String?
nil
end
def alt2_call(str : String) : String?
[str, nil].sample
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
private 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
Spectator.describe GitLabIssue51::Bar do
mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: ""
Spectator.describe Bar do
mock Foo, call: "", alt1_call: "", alt2_call: ""
let(:foo) { mock(Foo) }
let(:foo) { mock(GitLabIssue51::Foo) }
subject(:call) { described_class.new.call(foo) }
describe "#call" do

View 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

View File

@ -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,14 +289,14 @@ 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")
end
it "has the expected call listed" do
is_expected.to contain({:expected, "Not #{stub}"})
is_expected.to contain({:expected, "Not #{stub.message}"})
end
it "has the list of called methods" do

View File

@ -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])

View File

@ -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

View File

@ -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

View 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.
abstract_stub def self.foo : Nil
end
abstract_stub def self.foo(arg) : Nil
end
abstract_stub 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

View File

@ -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)

View File

@ -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
@ -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

View File

@ -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
@ -258,12 +258,12 @@ Spectator.describe "Mock DSL", :smoke do
# 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
@ -381,12 +381,12 @@ Spectator.describe "Mock DSL", :smoke do
# 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
@ -454,12 +454,12 @@ Spectator.describe "Mock DSL", :smoke do
# 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
@ -577,12 +577,12 @@ Spectator.describe "Mock DSL", :smoke do
# 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)
@ -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
abstract_stub 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
@ -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

View 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

View File

@ -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

View File

@ -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)

View File

@ -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
@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 %}

View File

@ -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.

View File

@ -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 %}

View File

@ -38,7 +38,7 @@ module Spectator::DSL
{{null_double_type_name}}.new(@stubs)
end
{% if block %}{{block.body}}{% end %}
{{block.body if block}}
end
{% begin %}
@ -218,24 +218,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}}
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
end
{% end %}
end
# Instantiates a mock.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -114,6 +114,21 @@ 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
@ -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
@ -160,9 +205,15 @@ module Spectator
stubbable._spectator_define_stub(unconstrained_stub)
end
# Apply the stub that is expected to be called.
stubbable._spectator_define_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._spectator_remove_stub(stub) }
end
# Asserts that some criteria defined by the matcher is eventually satisfied.
@ -190,9 +241,15 @@ module Spectator
stubbable._spectator_define_stub(unconstrained_stub)
end
# Apply the stub that could be called in case it is.
stubbable._spectator_define_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._spectator_remove_stub(stub) }
end
# :ditto:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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|

View File

@ -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 ("

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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 ("

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -59,7 +59,7 @@ module Spectator
# ```text
# FILE:LINE
# ```
def to_s(io)
def to_s(io : IO) : Nil
io << path << ':' << line
end
end

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,4 @@
require "../value"
require "./match_data"
module Spectator::Matchers
@ -22,6 +23,19 @@ module Spectator::Matchers
# A successful match with `#match` should normally fail for this method, and vice-versa.
abstract def negated_match(actual : Expression(T)) : MatchData forall T
# Compares a matcher against a value.
# Enables composable matchers.
def ===(actual : Expression(T)) : Bool
match(actual).matched?
end
# Compares a matcher against a value.
# Enables composable matchers.
def ===(other) : Bool
expression = Value.new(other)
match(expression).matched?
end
private def match_data_description(actual : Expression(T)) : String forall T
match_data_description(actual.label)
end

View File

@ -29,7 +29,26 @@ module Spectator::Matchers
# Checks whether the matcher is satisfied with the expression given to it.
private def match?(actual : Expression(T)) : Bool forall T
expected.value.includes?(actual.value)
actual_value = actual.value
expected_value = expected.value
if expected_value.is_a?(Range) && actual_value.is_a?(Comparable)
return match_impl?(expected_value, actual_value)
end
return false unless actual_value.is_a?(Comparable(typeof(expected_value.begin)))
expected_value.includes?(actual_value)
end
private def match_impl?(expected_value : Range(B, E), actual_value : Comparable(B)) : Bool forall B, E
expected_value.includes?(actual_value)
end
private def match_impl?(expected_value : Range(B, E), actual_value : T) : Bool forall B, E, T
return false unless actual_value.is_a?(B) || actual_value.is_a?(Comparable(B))
expected_value.includes?(actual_value)
end
private def match_impl?(expected_value : Range(Number, Number), actual_value : Number) : Bool
expected_value.includes?(actual_value)
end
# Message displayed when the matcher isn't satisfied.

View File

@ -81,7 +81,7 @@ module Spectator::Matchers
# Short text about the matcher's purpose.
def description : String
"received #{@stub} #{humanize_count}"
"received #{@stub.message} #{humanize_count}"
end
# Actually performs the test against the expression (value or block).
@ -89,10 +89,10 @@ module Spectator::Matchers
stubbed = actual.value
calls = relevant_calls(stubbed)
if @count.includes?(calls.size)
SuccessfulMatchData.new("#{actual.label} received #{@stub} #{humanize_count}")
SuccessfulMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}")
else
FailedMatchData.new("#{actual.label} received #{@stub} #{humanize_count}",
"#{actual.label} did not receive #{@stub}", values(actual).to_a)
FailedMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}",
"#{actual.label} did not receive #{@stub.message}", values(actual).to_a)
end
end
@ -106,9 +106,9 @@ module Spectator::Matchers
stubbed = actual.value
calls = relevant_calls(stubbed)
if @count.includes?(calls.size)
FailedMatchData.new("#{actual.label} did not receive #{@stub}", "#{actual.label} received #{@stub}", negated_values(actual).to_a)
FailedMatchData.new("#{actual.label} did not receive #{@stub.message}", "#{actual.label} received #{@stub.message}", negated_values(actual).to_a)
else
SuccessfulMatchData.new("#{actual.label} did not receive #{@stub} #{humanize_count}")
SuccessfulMatchData.new("#{actual.label} did not receive #{@stub.message} #{humanize_count}")
end
end
@ -120,7 +120,7 @@ module Spectator::Matchers
# Additional information about the match failure.
private def values(actual : Expression(T)) forall T
{
expected: @stub.to_s,
expected: @stub.message,
actual: method_call_list(actual.value),
}
end
@ -128,7 +128,7 @@ module Spectator::Matchers
# Additional information about the match failure when negated.
private def negated_values(actual : Expression(T)) forall T
{
expected: "Not #{@stub}",
expected: "Not #{@stub.message}",
actual: method_call_list(actual.value),
}
end

View File

@ -1,5 +1,61 @@
module Spectator
# Untyped arguments to a method call (message).
abstract class AbstractArguments
# Use the string representation to avoid over complicating debug output.
def inspect(io : IO) : Nil
to_s(io)
end
# Utility method for comparing two tuples considering special types.
private def compare_tuples(a : Tuple | Array, b : Tuple | Array)
return false if a.size != b.size
a.zip(b) do |a_value, b_value|
return false unless compare_values(a_value, b_value)
end
true
end
# Utility method for comparing two tuples considering special types.
# Supports nilable tuples (ideal for splats).
private def compare_tuples(a : Tuple? | Array?, b : Tuple? | Array?)
return false if a.nil? ^ b.nil?
compare_tuples(a.not_nil!, b.not_nil!)
end
# Utility method for comparing two named tuples ignoring order.
private def compare_named_tuples(a : NamedTuple | Hash, b : NamedTuple | Hash)
a.each do |k, v1|
v2 = b.fetch(k) { return false }
return false unless compare_values(v1, v2)
end
true
end
# Utility method for comparing two arguments considering special types.
# Some types used for case-equality don't work well with unexpected right-hand types.
# This can happen when the right side is a massive union of types.
private def compare_values(a, b)
case a
when Proc
# Using procs as argument matchers isn't supported currently.
# Compare directly instead.
a == b
when Range
# Ranges can only be matched against if their right side is comparable.
# Ensure the right side is comparable, otherwise compare directly.
return a === b if b.is_a?(Comparable(typeof(b)))
a == b
when Tuple, Array
return compare_tuples(a, b) if b.is_a?(Tuple) || b.is_a?(Array)
a === b
when NamedTuple, Hash
return compare_named_tuples(a, b) if b.is_a?(NamedTuple) || b.is_a?(Hash)
a === b
else
a === b
end
end
end
end

View File

@ -1,3 +1,4 @@
require "../harness"
require "./stub"
require "./stubbable"
require "./stubbed_type"
@ -21,6 +22,7 @@ module Spectator
# Applies a stub to the targeted stubbable object.
def to(stub : Stub) : Nil
@target._spectator_define_stub(stub)
Harness.current?.try &.cleanup { @target._spectator_remove_stub(stub) }
end
end
end

View File

@ -4,22 +4,19 @@ 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
# *Args* must be a `Tuple` representing the standard arguments.
# *KWArgs* must be a `NamedTuple` type representing extra keyword arguments.
class Arguments(Args, KWArgs) < AbstractArguments
# Positional arguments.
getter args : T
getter args : Args
# Keyword arguments.
getter kwargs : NT
getter kwargs : KWArgs
# 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)
def initialize(@args : Args, @kwargs : KWArgs)
{% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %}
{% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %}
end
# Instance of empty arguments.
@ -30,9 +27,14 @@ module Spectator
nil.as(AbstractArguments?)
end
# Friendlier constructor for capturing arguments.
def self.capture(*args, **kwargs)
new(args, kwargs)
end
# Returns the positional argument at the specified index.
def [](index : Int)
@args[index]
args[index]
end
# Returns the specified named argument.
@ -40,6 +42,16 @@ module Spectator
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
args
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
kwargs
end
# Constructs a string representation of the arguments.
def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && kwargs.empty?
@ -53,39 +65,46 @@ module Spectator
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)
kwargs.each_with_index(args.size) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
end
io << ')'
end
# Checks if this set of arguments and another are equal.
def ==(other : Arguments)
args == other.args && kwargs == other.kwargs
def ==(other : AbstractArguments)
positional == other.positional && 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)
compare_tuples(positional, other.positional) && compare_named_tuples(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
# :ditto:
def ===(other : FormalArguments)
return false unless compare_named_tuples(kwargs, other.named)
a.each do |k, v|
return false unless b.has_key?(k)
return false unless v === b[k]
i = 0
other.args.each do |k, v2|
break if i >= positional.size
next if kwargs.has_key?(k) # Covered by named arguments.
v1 = positional[i]
i += 1
return false unless compare_values(v1, v2)
end
true
other.splat.try &.each do |v2|
v1 = positional.fetch(i) { return false }
i += 1
return false unless compare_values(v1, v2)
end
i == positional.size
end
end
end

View File

@ -61,7 +61,7 @@ module Spectator
end
{% end %}
{% if block %}{{block.body}}{% end %}
{{block.body if block}}
end
end
@ -95,16 +95,38 @@ module Spectator
false
end
# Simplified string representation of a double.
# Avoids displaying nested content and bloating method instantiation.
def to_s(io : IO) : Nil
io << "#<" + {{@type.name(generic_args: false).stringify}} + " "
io << _spectator_stubbed_name << '>'
end
# :ditto:
def inspect(io : IO) : Nil
io << "#<" + {{@type.name(generic_args: false).stringify}} + " "
io << _spectator_stubbed_name
io << ":0x"
object_id.to_s(io, 16)
io << '>'
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}" }
Log.debug { "Defined stub for #{inspect} #{stub}" }
@stubs.unshift(stub)
end
protected def _spectator_remove_stub(stub : Stub) : Nil
Log.debug { "Removing stub #{stub} from #{inspect}" }
@stubs.delete(stub)
end
protected def _spectator_clear_stubs : Nil
Log.debug { "Clearing stubs for #{_spectator_stubbed_name}" }
Log.debug { "Clearing stubs for #{inspect}" }
@stubs.clear
end
@ -134,17 +156,17 @@ module Spectator
# 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}} + ">"
{{(anno[0] || :Anonymous.id).stringify}}
{% else %}
"#<Double Anonymous>"
"Anonymous"
{% end %}
end
private def self._spectator_stubbed_name : String
{% if anno = @type.annotation(StubbedName) %}
"#<Class Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
{{(anno[0] || :Anonymous.id).stringify}}
{% else %}
"#<Class Double Anonymous>"
"Anonymous"
{% end %}
end
@ -164,7 +186,7 @@ module Spectator
"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}")
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
end
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
@ -179,12 +201,13 @@ module Spectator
# 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 %})
args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}})
call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args)
_spectator_record_call(call)
raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
Log.trace { "#{inspect} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" }
raise ::Spectator::UnexpectedMessage.new("#{inspect} 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

View File

@ -20,6 +20,12 @@ module Spectator
def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil)
super(method, constraint, location)
end
# String representation of the stub, formatted as a method call.
def to_s(io : IO) : Nil
super
io << " # raises " << @exception
end
end
module StubModifiers

View File

@ -0,0 +1,133 @@
require "./abstract_arguments"
module Spectator
# Arguments passed into a method.
#
# *Args* must be a `NamedTuple` type representing the standard arguments.
# *Splat* must be a `Tuple` type representing the extra positional arguments.
# *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments.
class FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments
# Positional arguments.
getter args : Args
# Additional positional arguments.
getter splat : Splat
# Keyword arguments.
getter kwargs : DoubleSplat
# Name of the splat argument, if used.
getter splat_name : Symbol?
# Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
{% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %}
{% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %}
{% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %}
end
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
end
# Captures arguments passed to a call.
def self.build(args = NamedTuple.new, kwargs = NamedTuple.new)
new(args, nil, nil, kwargs)
end
# :ditto:
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
new(args, splat_name, splat, kwargs)
end
# Instance of empty arguments.
class_getter none : AbstractArguments = build
# Returns the positional argument at the specified index.
def [](index : Int)
positional[index]
end
# Returns the specified named argument.
def [](arg : Symbol)
return @args[arg] if @args.has_key?(arg)
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
if (splat = @splat)
args.values + splat
else
args.values
end
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
args.merge(kwargs)
end
# Constructs a string representation of the arguments.
def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty?
io << '('
# Add the positional arguments.
{% if Args < NamedTuple %}
# Include argument names.
args.each_with_index do |name, value, i|
io << ", " if i > 0
io << name << ": "
value.inspect(io)
end
{% else %}
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
{% end %}
# Add the splat arguments.
if (splat = @splat) && !splat.empty?
io << ", " unless args.empty?
if splat_name = !args.empty? && @splat_name
io << '*' << splat_name << ": {"
end
splat.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
io << '}' if splat_name
end
# Add the keyword arguments.
offset = args.size
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
end
io << ')'
end
# Checks if this set of arguments and another are equal.
def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs
end
# Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments)
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs)
end
# :ditto:
def ===(other : FormalArguments)
compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs)
end
end
end

View File

@ -26,15 +26,24 @@ module Spectator
super(_spectator_double_stubs + message_stubs)
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 raises an error.
protected def _spectator_define_stub(stub : Stub) : Nil
return super if Messages.types.has_key?(stub.method)
raise "Can't define stub #{stub} on lazy double because it wasn't initially defined."
end
# Returns the double's name formatted for user output.
private def _spectator_stubbed_name : String
"#<LazyDouble #{@name || "Anonymous"}>"
@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}")
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
else
Log.trace { "Fallback for #{call} - call original" }
yield
@ -43,13 +52,13 @@ module Spectator
# 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 %})
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}})
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
_spectator_record_call(%call)
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
# 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.

View File

@ -1,5 +1,6 @@
require "./abstract_arguments"
require "./arguments"
require "./formal_arguments"
module Spectator
# Stores information about a call to a method.
@ -16,13 +17,26 @@ module Spectator
# Creates a method call by splatting its arguments.
def self.capture(method : Symbol, *args, **kwargs)
arguments = Arguments.new(args, kwargs).as(AbstractArguments)
arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments)
new(method, arguments)
end
# Creates a method call from within a method.
# Takes the same arguments as `FormalArguments.build` but with the method name first.
def self.build(method : Symbol, *args, **kwargs)
arguments = FormalArguments.build(*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
io << '#' << method
arguments.inspect(io)
end
# :ditto:
def inspect(io : IO) : Nil
to_s(io)
end
end
end

View File

@ -1,5 +1,6 @@
require "./method_call"
require "./mocked"
require "./mock_registry"
require "./reference_mock_registry"
require "./stub"
require "./stubbed_name"
@ -36,7 +37,35 @@ module Spectator
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}}
{% if base.id == :module.id %}
{{base.id}} {{type_name.id}}
include {{mocked_type.id}}
# Mock class that includes the mocked module {{mocked_type.id}}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
private class ClassIncludingMock{{type_name.id}}
include {{type_name.id}}
end
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
inst = ClassIncludingMock{{type_name.id}}.allocate
inst.initialize(*args, **kwargs)
inst
end
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
inst = ClassIncludingMock{{type_name.id}}.allocate
inst.initialize(*args, **kwargs) { |*yargs| yield *yargs }
inst
end
{% else %}
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
{% end %}
include ::Spectator::Mocked
extend ::Spectator::StubbedType
@ -50,18 +79,22 @@ module Spectator
end
{% end %}
def _spectator_clear_stubs : Nil
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
@_spectator_stubs.try &.delete(stub)
end
def _spectator_clear_stubs : ::Nil
@_spectator_stubs = nil
end
private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub
class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
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
private def _spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
@ -69,7 +102,7 @@ module Spectator
\{% end %}
end
private def self._spectator_stubbed_name : String
private def self._spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Class Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
@ -80,7 +113,7 @@ module Spectator
macro finished
stub_type {{mocked_type.id}}
{% if block %}{{block.body}}{% end %}
{{block.body if block}}
end
end
{% end %}
@ -125,12 +158,12 @@ module Spectator
{% elsif base == :struct %}
@@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new
{% else %}
{% raise "Unsupported base type #{base} for injecting mock" %}
@@_spectator_mock_registry = ::Spectator::MockRegistry.new
{% end %}
private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub
class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
private def _spectator_stubs
entry = @@_spectator_mock_registry.fetch(self) do
@ -139,7 +172,11 @@ module Spectator
entry.stubs
end
def _spectator_clear_stubs : Nil
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
@@_spectator_mock_registry[self]?.try &.stubs.delete(stub)
end
def _spectator_clear_stubs : ::Nil
@@_spectator_mock_registry.delete(self)
end
@ -161,7 +198,7 @@ module Spectator
end
# Returns the mock's name formatted for user output.
private def _spectator_stubbed_name : String
private def _spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
@ -170,7 +207,7 @@ module Spectator
end
# Returns the mock's name formatted for user output.
private def self._spectator_stubbed_name : String
private def self._spectator_stubbed_name : ::String
\{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Class Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %}
@ -181,7 +218,7 @@ module Spectator
macro finished
stub_type {{type_name.id}}
{% if block %}{{block.body}}{% end %}
{{block.body if block}}
end
end
{% end %}

View File

@ -0,0 +1,43 @@
require "./mock_registry_entry"
require "./stub"
module Spectator
# Stores collections of stubs for mocked types.
#
# This type is intended for all mocked modules 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.
class MockRegistry
@entry : MockRegistryEntry?
# Retrieves all stubs.
def [](_object = nil)
@entry.not_nil!
end
# Retrieves all stubs.
def []?(_object = nil)
@entry
end
# Retrieves all stubs.
#
# 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))
entry = @entry
if entry.nil?
entry = MockRegistryEntry.new
entry.stubs = yield
@entry = entry
else
entry
end
end
# Clears all stubs defined for a mocked object.
def delete(object : Reference) : Nil
@entry = nil
end
end
end

View File

@ -26,6 +26,10 @@ module Spectator
_spectator_stubs.unshift(stub)
end
def _spectator_remove_stub(stub : Stub) : Nil
_spectator_stubs.delete(stub)
end
def _spectator_clear_stubs : Nil
_spectator_stubs.clear
end

View File

@ -26,7 +26,7 @@ module Spectator
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}")
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
else
Log.trace { "Fallback for #{call} - return self" }
self
@ -42,22 +42,22 @@ module Spectator
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}")
raise UnexpectedMessage.new("#{inspect} 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}`.")
raise TypeCastError.new("#{inspect} 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 %})
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}})
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
_spectator_record_call(%call)
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
self
end
end

View File

@ -25,6 +25,12 @@ module Spectator
@entries[key]
end
# Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet.
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.

View File

@ -22,6 +22,23 @@ module Spectator
def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil)
end
# String representation of the stub, formatted as a method call.
def message(io : IO) : Nil
io << "#" << method << (constraint || "(any args)")
end
# String representation of the stub, formatted as a method call.
def message
String.build do |str|
message(str)
end
end
# String representation of the stub, formatted as a method definition.
def to_s(io : IO) : Nil
message(io)
end
# Checks if a method call should receive the response from this stub.
def ===(call : MethodCall)
return false if method != call.method
@ -29,10 +46,5 @@ module Spectator
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

View File

@ -1,5 +1,5 @@
require "../dsl/reserved"
require "./arguments"
require "./formal_arguments"
require "./method_call"
require "./stub"
require "./typed_stub"
@ -28,6 +28,9 @@ module Spectator
# Defines a stub to change the behavior of a method.
abstract def _spectator_define_stub(stub : Stub) : Nil
# Removes a specific, previously defined stub.
abstract def _spectator_remove_stub(stub : Stub) : Nil
# Clears all previously defined stubs.
abstract def _spectator_clear_stubs : Nil
@ -115,7 +118,7 @@ module Spectator
{% 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}}(
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{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 %}
@ -123,25 +126,64 @@ module Spectator
{{method.body}}
end
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
{% original = "previous_def"
# Workaround for Crystal not propagating block with previous_def/super.
if method.accepts_block?
original += "("
if method.splat_index
method.args.each_with_index do |arg, i|
if i == method.splat_index
if arg.internal_name && arg.internal_name.size > 0
original += "*#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
elsif i > method.splat_index
original += "#{arg.name}: #{arg.internal_name}, "
else
original += "#{arg.internal_name}, "
end
end
else
method.args.each do |arg|
original += "#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
end
# If the block is captured (i.e. `&block` syntax), it must be passed along as an argument.
# Otherwise, use `yield` to forward the block.
captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
method.block_arg.name
else
nil
end
original += "&#{captured_block}" if captured_block
original += ")"
original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block
end
original = original.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}}(
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{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.build(
{{method.name.symbolize}},
::NamedTuple.new(
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
),
{% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
::NamedTuple.new(
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
).merge({{method.double_splat}})
)
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments.
@ -150,10 +192,24 @@ module Spectator
# 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
{{ if rt = method.return_type
if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn
:no_return
else
# Process as an enumerable type to reduce code repetition.
rt = rt.is_a?(Union) ? rt.types : [rt]
# Check if any types are nilable.
nilable = rt.any? do |t|
# These are all macro types that have the `resolve?` method.
(t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) &&
(resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil
end
if nilable
:nil
else
:raise
end
end
else
:raise
end }})
@ -211,7 +267,7 @@ module Spectator
%}
{% unless method.abstract? %}
{{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{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 %}
@ -219,7 +275,42 @@ module Spectator
{{method.body}}
end
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
{% original = "previous_def"
# Workaround for Crystal not propagating block with previous_def/super.
if method.accepts_block?
original += "("
if method.splat_index
method.args.each_with_index do |arg, i|
if i == method.splat_index
if arg.internal_name && arg.internal_name.size > 0
original += "*#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
elsif i > method.splat_index
original += "#{arg.name}: #{arg.internal_name}"
else
original += "#{arg.internal_name}, "
end
end
else
method.args.each do |arg|
original += "#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
end
# If the block is captured (i.e. `&block` syntax), it must be passed along as an argument.
# Otherwise, use `yield` to forward the block.
captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
method.block_arg.name
else
nil
end
original += "&#{captured_block}" if captured_block
original += ")"
original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block
end
original = original.id %}
{% end %}
{% # Reconstruct the method signature.
@ -227,18 +318,23 @@ module Spectator
# 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}}(
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{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.build(
{{method.name.symbolize}},
::NamedTuple.new(
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
),
{% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
::NamedTuple.new(
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
).merge({{method.double_splat}})
)
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments.
@ -246,15 +342,25 @@ module Spectator
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 %}
{% if rt = 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
{{ if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= 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
# Process as an enumerable type to reduce code repetition.
rt = rt.is_a?(Union) ? rt.types : [rt]
# Check if any types are nilable.
nilable = rt.any? do |t|
# These are all macro types that have the `resolve?` method.
(t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) &&
(resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil
end
if nilable
:nil
else
:raise
end
end }})
{% elsif !method.abstract? %}
# The method isn't abstract, infer the type it returns without calling it.
@ -325,67 +431,96 @@ module Spectator
# 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 %}
definitions = [] of Nil
scope = if type == @type
:previous_def
elsif type.module?
type.name
else
:super
end.id
{% 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 %}
# Add entries for methods in the target type and its class type.
[[:self.id, type.class], [nil, type]].each do |(receiver, t)|
t.methods.each do |method|
definitions << {
type: t,
method: method,
scope: scope,
receiver: receiver,
}
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}}(
# Iterate through all ancestors and add their methods.
type.ancestors.each do |ancestor|
[[:self.id, ancestor.class], [nil, ancestor]].each do |(receiver, t)|
t.methods.each do |method|
# Skip methods already found to prevent redefining them multiple times.
unless definitions.any? do |d|
m = d[:method]
m.name == method.name &&
m.args == method.args &&
m.splat_index == method.splat_index &&
m.double_splat == method.double_splat &&
m.block_arg == method.block_arg
end
definitions << {
type: t,
method: method,
scope: :super.id,
receiver: receiver,
}
end
end
end
end
definitions = definitions.reject do |definition|
name = definition[:method].name
name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.symbolize)
end %}
{% for definition in definitions %}
{% original_type = definition[:type]
method = definition[:method]
scope = definition[:scope]
receiver = definition[:receiver]
rewrite_args = method.accepts_block?
# Handle calling methods on other objects (primarily for mock modules).
if scope != :super.id && scope != :previous_def.id
if receiver == :self.id
scope = "#{scope}.#{method.name}".id
rewrite_args = true
else
scope = :super.id
end
end %}
# Redefinition of {{original_type}}{{"#".id}}{{method.name}}
{{(method.abstract? ? "abstract_stub abstract" : "default_stub").id}} {{method.visibility.id if method.visibility != :public}} def {{"#{receiver}.".id if receiver}}{{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 %}
{% unless method.abstract? %}
{{scope}}{% if rewrite_args %}({% for arg, i in method.args %}
{% if i == method.splat_index && arg.internal_name && arg.internal_name.size > 0 %}*{{arg.internal_name}}, {% if method.double_splat %}**{{method.double_splat}}, {% end %}{% end %}
{% if method.splat_index && i > method.splat_index %}{{arg.name}}: {{arg.internal_name}}, {% end %}
{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name}}, {% end %}{% end %}
{% if !method.splat_index && method.double_splat %}**{{method.double_splat}}, {% end %}
{% captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
method.block_arg.name
else
nil
end %}
{% if captured_block %}&{{captured_block}}{% end %}
){% if !captured_block && method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}{% 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.
# Utility macro for casting a stub (and its return value) to the correct type.
#
# *stub* is the variable holding the stub.
# *call* is the variable holding the captured method call.
@ -395,49 +530,38 @@ module Spectator
# - `: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.
{% 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}})
%type = {{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)
# 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.
%cast = %value.as?({{type}})
{% 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
{% if fail_cast == :nil %}
%cast
{% elsif fail_cast == :raise %}
# Check if nil was returned by the stub and if its okay to return it.
if %value.nil? && %type.nilable?
# Value was nil and nil is allowed to be returned.
%type.cast(%cast)
elsif %cast.nil?
# The stubbed value was something else entirely and cannot be cast to the return type.
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.")
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 %}
# Types match and value can be returned as cast type.
%cast
end
{% else %}
{% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %}
{% end %}
end
{% end %}
end
end
end

View File

@ -20,6 +20,10 @@ module Spectator
_spectator_stubs.unshift(stub)
end
def _spectator_remove_stub(stub : Stub) : Nil
_spectator_stubs.delete(stub)
end
def _spectator_clear_stubs : Nil
_spectator_stubs.clear
end

View File

@ -9,5 +9,11 @@ module Spectator
abstract class TypedStub(T) < Stub
# Invokes the stubbed implementation.
abstract def call(call : MethodCall) : T
# String representation of the stub, formatted as a method call.
def to_s(io : IO) : Nil
super
io << " : " << T
end
end
end

View File

@ -29,6 +29,12 @@ module Spectator
@entries[key]
end
# Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet.
def []?(object : T)
key = value_bytes(object)
@entries[key]?
end
# Retrieves all stubs defined for a mocked object.
#
# Yields to the block on the first retrieval.

View File

@ -20,6 +20,13 @@ module Spectator
def initialize(method : Symbol, @value : T, constraint : AbstractArguments? = nil, location : Location? = nil)
super(method, constraint, location)
end
# String representation of the stub, formatted as a method call and return value.
def to_s(io : IO) : Nil
super
io << " # => "
@value.inspect(io)
end
end
module StubModifiers

View File

@ -30,14 +30,16 @@ module Spectator
end
# User-defined tags and values used for filtering and behavior modification.
getter metadata : Metadata
def metadata : Metadata
@metadata ||= Metadata.new
end
# Creates the node.
# The *name* describes the purpose of the node.
# It can be a `Symbol` to describe a type.
# The *location* tracks where the node exists in source code.
# A set of *metadata* can be used for filtering and modifying example behavior.
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new)
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil)
end
# Indicates whether the node has completed.
@ -46,17 +48,25 @@ module Spectator
# Checks if the node has been marked as pending.
# Pending items should be skipped during execution.
def pending?
metadata.has_key?(:pending) || metadata.has_key?(:skip)
return false unless md = @metadata
md.has_key?(:pending) || md.has_key?(:skip)
end
# Gets the reason the node has been marked as pending.
def pending_reason
metadata[:pending]? || metadata[:skip]? || metadata[:reason]? || DEFAULT_PENDING_REASON
return DEFAULT_PENDING_REASON unless md = @metadata
md[:pending]? || md[:skip]? || md[:reason]? || DEFAULT_PENDING_REASON
end
# Retrieves just the tag names applied to the node.
def tags
Tags.new(metadata.keys)
if md = @metadata
Tags.new(md.keys)
else
Tags.new
end
end
# Non-nil name used to show the node name.
@ -66,12 +76,12 @@ module Spectator
# Constructs the full name or description of the node.
# This prepends names of groups this node is part of.
def to_s(io)
def to_s(io : IO) : Nil
display_name.to_s(io)
end
# Exposes information about the node useful for debugging.
def inspect(io)
def inspect(io : IO) : Nil
# Full node name.
io << '"' << self << '"'

View File

@ -9,7 +9,7 @@ module Spectator
end
# Calls the `pass` method on *visitor*.
def accept(visitor)
def accept(visitor, &)
visitor.pass(yield self)
end
@ -24,7 +24,7 @@ module Spectator
end
# One-word description of the result.
def to_s(io)
def to_s(io : IO) : Nil
io << "pass"
end

View File

@ -11,7 +11,7 @@ module Spectator
# The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`.
# A default *reason* can be given in case the user didn't provide one.
def initialize(@name : String? = nil, @location : Location? = nil,
@metadata : Metadata = Metadata.new, @reason : String? = nil)
@metadata : Metadata? = nil, @reason : String? = nil)
end
# Constructs an example with previously defined attributes.

View File

@ -28,7 +28,7 @@ module Spectator
end
# Calls the `pending` method on the *visitor*.
def accept(visitor)
def accept(visitor, &)
visitor.pending(yield self)
end
@ -43,7 +43,7 @@ module Spectator
end
# One-word description of the result.
def to_s(io)
def to_s(io : IO) : Nil
io << "pending"
end

View File

@ -22,51 +22,106 @@ class Object
# ```
# require "spectator/should"
# ```
def should(matcher, message = nil)
def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
actual = ::Spectator::Value.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.match(actual)
expectation = ::Spectator::Expectation.new(match_data, message: message)
expectation = ::Spectator::Expectation.new(match_data, location, message)
::Spectator::Harness.current.report(expectation)
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 should(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U
actual = ::Spectator::Value.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.match(actual)
expectation = ::Spectator::Expectation.new(match_data, location, message)
if ::Spectator::Harness.current.report(expectation)
return self if self.is_a?(U)
raise "Spectator bug: expected value should have cast to #{U}"
else
raise TypeCastError.new("Expected value should be a #{U}, but was actually #{self.class}")
end
end
# Works the same as `#should` except the condition is inverted.
# When `#should` succeeds, this method will fail, and vice-versa.
def should_not(matcher, message = nil)
def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
actual = ::Spectator::Value.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.negated_match(actual)
expectation = ::Spectator::Expectation.new(match_data, message: message)
expectation = ::Spectator::Expectation.new(match_data, location, message)
::Spectator::Harness.current.report(expectation)
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 should_not(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U
actual = ::Spectator::Value.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.negated_match(actual)
expectation = ::Spectator::Expectation.new(match_data, location, message)
if ::Spectator::Harness.current.report(expectation)
return self unless self.is_a?(U)
raise "Spectator bug: expected value should not be #{U}"
else
raise TypeCastError.new("Expected value is not expected to be a #{U}, but was actually #{self.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 should_not(matcher : ::Spectator::Matchers::NilMatcher, message = nil, *, _file = __FILE__, _line = __LINE__)
actual = ::Spectator::Value.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.negated_match(actual)
expectation = ::Spectator::Expectation.new(match_data, location, message)
if ::Spectator::Harness.current.report(expectation)
return self unless self.nil?
raise "Spectator bug: expected value should not be nil"
else
raise NilAssertionError.new("Expected value should not be nil.")
end
end
# Works the same as `#should` except that the condition check is postponed.
# The expectation is checked after the example finishes and all hooks have run.
def should_eventually(matcher, message = nil)
::Spectator::Harness.current.defer { should(matcher, message) }
def should_eventually(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
::Spectator::Harness.current.defer { should(matcher, message, _file: _file, _line: _line) }
end
# Works the same as `#should_not` except that the condition check is postponed.
# The expectation is checked after the example finishes and all hooks have run.
def should_never(matcher, message = nil)
::Spectator::Harness.current.defer { should_not(matcher, message) }
def should_never(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
::Spectator::Harness.current.defer { should_not(matcher, message, _file: _file, _line: _line) }
end
end
struct Proc(*T, R)
# Extension method to create an expectation for a block of code (proc).
# Depending on the matcher, the proc may be executed multiple times.
def should(matcher, message = nil)
def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
actual = ::Spectator::Block.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.match(actual)
expectation = ::Spectator::Expectation.new(match_data, message: message)
expectation = ::Spectator::Expectation.new(match_data, location, message)
::Spectator::Harness.current.report(expectation)
end
# Works the same as `#should` except the condition is inverted.
# When `#should` succeeds, this method will fail, and vice-versa.
def should_not(matcher, message = nil)
def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
actual = ::Spectator::Block.new(self)
location = ::Spectator::Location.new(_file, _line)
match_data = matcher.negated_match(actual)
expectation = ::Spectator::Expectation.new(match_data, message: message)
expectation = ::Spectator::Expectation.new(match_data, location, message)
::Spectator::Harness.current.report(expectation)
end
end

View File

@ -60,7 +60,7 @@ module Spectator
#
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
def start_group(name, location = nil, metadata = Metadata.new) : Nil
def start_group(name, location = nil, metadata = nil) : Nil
Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" }
builder = ExampleGroupBuilder.new(name, location, metadata)
@ -86,7 +86,7 @@ module Spectator
#
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = Metadata.new) : Nil
def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = nil) : Nil
Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" }
builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata)
@ -127,7 +127,7 @@ module Spectator
# It will be yielded two arguments - the example created by this method, and the *context* argument.
# The return value of the block is ignored.
# It is expected that the test code runs when the block is called.
def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) : Nil
def add_example(name, location, context_builder, metadata = nil, &block : Example -> _) : Nil
Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" }
current << ExampleBuilder.new(context_builder, block, name, location, metadata)
end
@ -144,7 +144,7 @@ module Spectator
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark the test as pending and skip execution.
# A default *reason* can be given in case the user didn't provide one.
def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Nil
def add_pending_example(name, location, metadata = nil, reason = nil) : Nil
Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" }
current << PendingExampleBuilder.new(name, location, metadata, reason)
end

Some files were not shown because too many files have changed in this diff Show More