Merge remote-tracking branch 'origin/master' into release/0.10

This commit is contained in:
Michael Miller 2021-07-11 00:16:25 -06:00
commit 6c55301d0b
No known key found for this signature in database
GPG key ID: F9A0C5C65B162436
11 changed files with 140 additions and 12 deletions

View file

@ -40,6 +40,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- Removed one-liner `it`-syntax without braces (block). - Removed one-liner `it`-syntax without braces (block).
## [0.9.40] - 2021-07-10
### Fixed
- Fix stubbing of class methods.
- Fix handling of `no_args` in some cases.
### Changed
- Better handling and stubbing of `Process.exit`.
## [0.9.39] - 2021-07-02
### Fixed
- Fix `expect().to receive()` syntax not implicitly stubbing the method.
- Avoid calling `NoReturn` methods from stubs. [#29](https://github.com/icy-arctic-fox/spectator/issues/29)
### Added
- Added support for `with(no_args)` for method stubs. [#28](https://github.com/icy-arctic-fox/spectator/issues/28)
- Allow creation of doubles without definition block. [#30](https://github.com/icy-arctic-fox/spectator/issues/30)
## [0.9.38] - 2021-05-27 ## [0.9.38] - 2021-05-27
### Fixed ### Fixed
- Fix `Channel::ClosedError` when using default Crystal Logger. [#27](https://github.com/icy-arctic-fox/spectator/issues/27) - Fix `Channel::ClosedError` when using default Crystal Logger. [#27](https://github.com/icy-arctic-fox/spectator/issues/27)
@ -301,7 +318,8 @@ This has been changed so that it compiles and raises an error at runtime with a
First version ready for public use. First version ready for public use.
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.39...release%2F0.10 [Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.40...release%2F0.10
[0.9.40]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.39...v0.9.40
[0.9.39]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.38...v0.9.39 [0.9.39]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.38...v0.9.39
[0.9.38]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.37...v0.9.38 [0.9.38]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.37...v0.9.38
[0.9.37]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.36...v0.9.37 [0.9.37]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.36...v0.9.37

View file

@ -17,4 +17,24 @@ Spectator.describe "GitHub Issue #29" do
subject.goodbye subject.goodbye
end end
end end
describe "class method" do
class Foo
def self.test
exit 0
end
end
mock Foo do
stub self.exit(code)
end
subject { Foo }
it "must capture exit" do
expect(subject).to receive(:exit).with(0)
subject.test
end
end
end end

View file

@ -0,0 +1,37 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #32" do
module TestFoo
class TestClass
def initialize
end
# the method we are testing
def self.test
new().test
end
# the method we want to ensure gets called
def test
end
end
end
let(test_class) { TestFoo::TestClass }
let(test_instance) { test_class.new }
describe "something else" do
mock TestFoo::TestClass do
stub self.new
stub test
end
it "must test when new is called" do
expect(test_class).to receive(:new).with(no_args).and_return(test_instance)
expect(test_instance).to receive(:test)
expect(test_class.new).to be(test_instance)
test_class.test
end
end
end

View file

@ -0,0 +1,26 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #33" do
class Test
def method2
end
def method1
method2
end
end
mock Test do
stub method2
end
describe Test do
describe "#method1" do
it do
expect(subject).to receive(:method2)
subject.method1
end
end
end
end

View file

@ -98,6 +98,7 @@ module Spectator
elapsed = Time.measure do elapsed = Time.measure do
error = catch { yield } error = catch { yield }
end end
error = nil if error.is_a?(SystemExit) && mocks.exit_handled?
{elapsed, error} {elapsed, error}
end end

View file

@ -1,4 +1,5 @@
require "./mocks/*" require "./mocks/*"
require "./system_exit"
module Spectator module Spectator
# Functionality for mocking existing types. # Functionality for mocking existing types.
@ -11,3 +12,13 @@ module Spectator
end end
end end
end end
# Add default stub to `exit` method.
# This captures *most* (technically not all) attempts to exit the process.
# This stub only takes effect in example code.
# It intercepts `exit` calls and raises `Spectator::SystemExit` to prevent killing the test.
class ::Process
include ::Spectator::Mocks::Stubs
stub self.exit(code) { raise ::Spectator::SystemExit.new }
end

View file

@ -20,6 +20,8 @@ module Spectator::Mocks
value = call(args) { |*ya| yield *ya } value = call(args) { |*ya| yield *ya }
if value.is_a?(RT) if value.is_a?(RT)
value.as(RT) value.as(RT)
elsif value.nil? && RT == NoReturn
raise SystemExit.new
else else
raise TypeCastError.new("The return type of stub #{self} doesn't match the expected type #{RT}") raise TypeCastError.new("The return type of stub #{self} doesn't match the expected type #{RT}")
end end

View file

@ -41,6 +41,10 @@ module Spectator::Mocks
NilMethodStub.new(@name, @location, args) NilMethodStub.new(@name, @location, args)
end end
def with(args : Arguments)
NilMethodStub.new(@name, @location, @args)
end
def and_call_original def and_call_original
OriginalMethodStub.new(@name, @location, @args) OriginalMethodStub.new(@name, @location, @args)
end end

View file

@ -35,6 +35,10 @@ module Spectator::Mocks
fetch_type(object.class).stubs.find(&.callable?(call)) fetch_type(object.class).stubs.find(&.callable?(call))
end end
def find_stub(type : T.class, call : MethodCall) forall T
fetch_type(type).stubs.find(&.callable?(call))
end
def record_call(object, call : MethodCall) : Nil def record_call(object, call : MethodCall) : Nil
fetch_instance(object).calls << call fetch_instance(object).calls << call
fetch_type(object.class).calls << call fetch_type(object.class).calls << call
@ -53,6 +57,14 @@ module Spectator::Mocks
fetch_type(object.class).expected.any?(&.callable?(call)) fetch_type(object.class).expected.any?(&.callable?(call))
end end
def exit_handled? : Bool
# Lazily check if an `exit` method was called and it was expected.
# This is okay since an `expect().to receive(:exit)` should check the details of the call.
(@entries.any? { |_key, entry| entry.expected.any? { |stub| stub.name == :exit } } ||
@all_instances.any? { |_key, entry| entry.expected.any? { |stub| stub.name == :exit } }) &&
@entries.any? { |_key, entry| entry.calls.any? { |call| call.name == :exit } }
end
def expect(object, stub : MethodStub) : Nil def expect(object, stub : MethodStub) : Nil
entry = fetch_instance(object) entry = fetch_instance(object)
entry.expected.add(stub) entry.expected.add(stub)

View file

@ -62,7 +62,7 @@ module Spectator::Mocks
elsif t.has_method?(name) elsif t.has_method?(name)
:previous_def :previous_def
else else
"::#{name}" name
end.id end.id
%} %}
@ -82,12 +82,8 @@ module Spectator::Mocks
%call = ::Spectator::Mocks::MethodCall.new({{name.symbolize}}, %args) %call = ::Spectator::Mocks::MethodCall.new({{name.symbolize}}, %args)
%harness.mocks.record_call(self, %call) %harness.mocks.record_call(self, %call)
if (%stub = %harness.mocks.find_stub(self, %call)) if (%stub = %harness.mocks.find_stub(self, %call))
if typeof({{original}}) == NoReturn
return %stub.call!(%args) { nil }
else
return %stub.call!(%args) { {{original}} } return %stub.call!(%args) { {{original}} }
end end
end
{% if body && !body.is_a?(Nop) || return_type.is_a?(ArrayLiteral) %} {% if body && !body.is_a?(Nop) || return_type.is_a?(ArrayLiteral) %}
%method({{args.splat}}) %method({{args.splat}})
@ -105,12 +101,8 @@ module Spectator::Mocks
%call = ::Spectator::Mocks::MethodCall.new({{name.symbolize}}, %args) %call = ::Spectator::Mocks::MethodCall.new({{name.symbolize}}, %args)
%harness.mocks.record_call(self, %call) %harness.mocks.record_call(self, %call)
if (%stub = %harness.mocks.find_stub(self, %call)) if (%stub = %harness.mocks.find_stub(self, %call))
if typeof({{original}}) == NoReturn
return %stub.call!(%args) { nil }
else
return %stub.call!(%args) { {{original}} { |*%ya| yield *%ya } } return %stub.call!(%args) { {{original}} { |*%ya| yield *%ya } }
end end
end
{% if body && !body.is_a?(Nop) || return_type.is_a?(ArrayLiteral) %} {% if body && !body.is_a?(Nop) || return_type.is_a?(ArrayLiteral) %}
%method({{args.splat}}) { |*%ya| yield *%ya } %method({{args.splat}}) { |*%ya| yield *%ya }

View file

@ -0,0 +1,5 @@
module Spectator
# Exception raised when `exit` is called and intercepted from a stub.
class SystemExit < Exception
end
end