├── .travis.yml ├── Projectfile ├── src ├── mock │ ├── version.cr │ ├── arguments.cr │ ├── method_stub.cr │ └── double.cr └── mock.cr ├── shard.yml ├── .gitignore ├── spec ├── spec_helper.cr ├── arguments_spec.cr ├── example_spec.cr ├── method_stub_spec.cr └── mock_spec.cr ├── LICENSE └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /Projectfile: -------------------------------------------------------------------------------- 1 | deps do 2 | end 3 | -------------------------------------------------------------------------------- /src/mock/version.cr: -------------------------------------------------------------------------------- 1 | module Mock 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: mock 2 | version: 0.0.1 3 | 4 | authors: 5 | - Sergio Gil Pérez de la Manga 6 | 7 | license: MIT 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.deps/ 2 | /libs/ 3 | /.crystal/ 4 | 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in application that uses them 8 | /.deps.lock 9 | 10 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/mock" 3 | 4 | class MyClass 5 | def a_method 6 | "result" 7 | end 8 | end 9 | 10 | class MyOtherClass 11 | def a_method_with_arguments(argument) 12 | argument.a_method 13 | end 14 | 15 | def a_method_with_restricted_arguments(argument : MyClass) 16 | argument.a_method 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/mock/arguments.cr: -------------------------------------------------------------------------------- 1 | module Mock 2 | class Arguments 3 | def initialize(arguments) 4 | @arguments = arguments.to_a 5 | end 6 | 7 | def_equals @arguments 8 | 9 | def to_s 10 | @arguments.inspect 11 | end 12 | 13 | def self.empty 14 | Empty.new 15 | end 16 | 17 | class Empty < Arguments 18 | def initialize 19 | end 20 | 21 | def ==(other) 22 | false 23 | end 24 | 25 | def ==(other : Empty) 26 | true 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/arguments_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Mock::Arguments do 4 | describe ".empty" do 5 | it "is equal to other empty arguments" do 6 | Mock::Arguments.empty.should eq(Mock::Arguments.empty) 7 | end 8 | 9 | it "is not equal to other non empty arguments" do 10 | Mock::Arguments.empty.should_not eq(Mock::Arguments.new(Array(Int32).new)) 11 | Mock::Arguments.empty.should_not eq(Mock::Arguments.new([1])) 12 | Mock::Arguments.empty.should_not eq(Mock::Arguments.new(["a", "b"])) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/mock.cr: -------------------------------------------------------------------------------- 1 | require "./mock/*" 2 | 3 | module Mock 4 | class UnexpectedCall < Exception 5 | end 6 | 7 | alias DoublesRegistry = Array(Mock::Double) 8 | 9 | @@registry = DoublesRegistry.new 10 | 11 | def self.register(double) 12 | @@registry << double 13 | end 14 | 15 | def self.reset 16 | @@registry.clear 17 | end 18 | 19 | def self.registry 20 | @@registry 21 | end 22 | end 23 | 24 | module Spec::DSL 25 | def double(*args) 26 | Mock::Double.new(*args) 27 | end 28 | 29 | def it(description, file = __FILE__, line = __LINE__) 30 | Mock.reset 31 | previous_def 32 | Mock.registry.each &.check_expectations 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /src/mock/method_stub.cr: -------------------------------------------------------------------------------- 1 | module Mock 2 | class MethodStub 3 | getter :method_name, :arguments, :value 4 | 5 | def initialize(@method_name) 6 | end 7 | 8 | def with(arguments : Arguments) 9 | @arguments = arguments 10 | self 11 | end 12 | 13 | def with(*args) 14 | @arguments = if args.empty? 15 | Arguments.empty 16 | else 17 | Arguments.new(args) 18 | end 19 | 20 | self 21 | end 22 | 23 | def and_return(@value) 24 | self 25 | end 26 | end 27 | 28 | class MethodStubCollection 29 | def initialize 30 | @stubs = [] of MethodStub 31 | end 32 | 33 | delegate :<<, @stubs 34 | 35 | def each 36 | @stubs.each { |s| yield s } 37 | end 38 | 39 | def find(stub : MethodStub) 40 | find(stub.method_name, stub.arguments) 41 | end 42 | 43 | def find(method_name, arguments) 44 | @stubs.find do |stub| 45 | method_name == stub.method_name && (stub.arguments.nil? || arguments.nil? || arguments == stub.arguments) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sergio Gil Pérez de la Manga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/example_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Mock do 4 | it "stubs a method's return value" do 5 | my_instance = double() 6 | my_instance.stub(:a_method).and_return("stubbed result") 7 | 8 | my_instance.a_method.should eq("stubbed result") 9 | end 10 | 11 | it "stubs a method's return value (short version)" do 12 | my_instance = double(:my_object, { :a_method => "stubbed result" }) 13 | 14 | my_instance.a_method.should eq("stubbed result") 15 | end 16 | 17 | it "filters by arguments" do 18 | my_instance = double() 19 | my_instance.stub(:a_method).with(123).and_return("123") 20 | my_instance.stub(:a_method).with(456).and_return("456") 21 | 22 | my_instance.a_method(123).should eq("123") 23 | my_instance.a_method(456).should eq("456") 24 | end 25 | 26 | it "arguments can be ignored" do 27 | my_instance = double() 28 | my_instance.stub(:a_method).and_return("whatever") 29 | 30 | my_instance.a_method.should eq("whatever") 31 | my_instance.a_method(123).should eq("whatever") 32 | my_instance.a_method(456).should eq("whatever") 33 | end 34 | 35 | it "stubbed explicitly without arguments matches only without arguments" do 36 | my_instance = double() 37 | my_instance.stub(:a_method).with().and_return("hello") 38 | 39 | my_instance.a_method.should eq("hello") 40 | 41 | # this would fail: 42 | # my_instance.a_method(456) 43 | end 44 | 45 | it "expectations can be set" do 46 | my_instance = double() 47 | 48 | my_instance.should_receive(:a_method).and_return("whatever") 49 | 50 | # test would fail if we removed this line: 51 | my_instance.a_method.should eq("whatever") 52 | end 53 | 54 | it "filtering by arguments affect expectations too" do 55 | my_instance = double() 56 | 57 | my_instance.should_receive(:a_method_with_arguments).with("hello").and_return("world") 58 | 59 | my_instance.a_method_with_arguments("hello").should eq("world") 60 | 61 | # this would fail: 62 | # my_instance.a_method_with_arguments("bye") 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /src/mock/double.cr: -------------------------------------------------------------------------------- 1 | module Mock 2 | class Double 3 | def initialize(@name = :double, stubs = nil) 4 | @stubs = MethodStubCollection.new 5 | @expectations = MethodStubCollection.new 6 | @negative_expectations = MethodStubCollection.new 7 | @calls = MethodStubCollection.new 8 | Mock.register self 9 | add_stubs(stubs) if stubs 10 | end 11 | 12 | private def add_stubs(stubs) 13 | stubs.each do |method_name, value| 14 | stub(method_name).and_return(value) 15 | end 16 | end 17 | 18 | def stub(method_name) 19 | MethodStub.new(method_name).tap do |stub| 20 | @stubs << stub 21 | end 22 | end 23 | 24 | def should_receive(method_name) 25 | stub(method_name).tap do |stub| 26 | @expectations << stub 27 | end 28 | end 29 | 30 | def should_not_receive(method_name) 31 | MethodStub.new(method_name).tap do |stub| 32 | @negative_expectations << stub 33 | end 34 | end 35 | 36 | def check_expectations 37 | @expectations.each do |expectation| 38 | @calls.should HaveCalledExpectation.new(expectation) 39 | end 40 | @negative_expectations.each do |expectation| 41 | @calls.should_not HaveCalledExpectation.new(expectation) 42 | end 43 | end 44 | 45 | macro method_missing(name, args, block) 46 | {% if args.empty? %} 47 | arguments = Arguments.empty 48 | {% else %} 49 | arguments = Arguments.new({{args}}) 50 | {% end %} 51 | 52 | if stub = @stubs.find(:{{name}}, arguments) 53 | @calls << MethodStub.new(:{{name}}).with(arguments) 54 | stub.value 55 | else 56 | raise UnexpectedCall.new("Unexpected call to #{{{name}}} on double #{@name.inspect}") 57 | end 58 | end 59 | end 60 | 61 | class HaveCalledExpectation 62 | def initialize(@expectation) 63 | end 64 | 65 | def match(calls) 66 | calls.find(@expectation) 67 | end 68 | 69 | def failure_message 70 | "expected #{@expectation.method_name} to be called with arguments #{@expectation.arguments.to_s}, but wasn't" 71 | end 72 | 73 | def negative_failure_message 74 | "expected #{@expectation.method_name} to not be called with arguments #{@expectation.arguments.to_s}, but was" 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mock [![Build Status](https://travis-ci.org/porras/mock.svg?branch=master)](https://travis-ci.org/porras/mock) 2 | 3 | Mock is a doubles (stubs and mocks) library for [Crystal](http://crystal-lang.org/), inspired by the API of [rspec-mocks](https://github.com/rspec/rspec-mocks) (but it implements a small basic subset of it to the date). 4 | 5 | ## Installation 6 | 7 | Add this to your application's shard.yml: 8 | 9 | ```yaml 10 | development_dependencies: 11 | mock: 12 | github: porras/mock 13 | ``` 14 | 15 | You can now run `shards` to install it. 16 | 17 | ## Usage 18 | 19 | Require it in your tests and you can start using it. 20 | 21 | ```crystal 22 | require "mock" 23 | ``` 24 | 25 | ### Creating a double 26 | 27 | Just call the `double()` method. 28 | 29 | ### Stubbing a method 30 | 31 | Calling `stub` on that double object will set a method stub: 32 | 33 | ```crystal 34 | my_object = double() 35 | my_object.stub(:my_method) 36 | ``` 37 | 38 | You can establish a return value for the stub method (if you don't, method stubs return `nil`): 39 | 40 | ```crystal 41 | my_object = double() 42 | my_object.stub(:my_method).and_return("my value") 43 | 44 | my_object.my_method.should eq("my value") 45 | ``` 46 | 47 | You can also filter my arguments, establishing different stubs for the same method: 48 | 49 | ```crystal 50 | my_object = double() 51 | my_object.stub(:my_method).with(1).and_return("value 1") 52 | my_object.stub(:my_method).with(2).and_return("value 2") 53 | 54 | my_object.my_method(1).should eq("value 1") 55 | my_object.my_method(2).should eq("value 2") 56 | ``` 57 | 58 | ### Setting expectations 59 | 60 | You can also set the expectation that a method will be called, and it will be automatically checked at the end of the test: 61 | 62 | ```crystal 63 | my_object = double() 64 | my_object.should_receive(:my_method).with(1).and_return("my value") 65 | 66 | # if we omit this line, the test will fail 67 | my_object.my_method(1).should eq("value 1") 68 | ``` 69 | 70 | See [example_spec.cr](https://github.com/porras/mock/blob/master/spec/example_spec.cr) for more examples. 71 | 72 | ## Contributing 73 | 74 | 1. Fork it ( https://github.com/porras/mock/fork ) 75 | 2. Create your feature branch (git checkout -b my-new-feature) 76 | 3. Commit your changes (git commit -am 'Add some feature') 77 | 4. Push to the branch (git push origin my-new-feature) 78 | 5. Create a new Pull Request 79 | 80 | ## License 81 | 82 | This code is released under the [MIT License](https://github.com/porras/mock/blob/master/LICENSE). 83 | 84 | ## Contributors 85 | 86 | - [Sergio Gil](http://iamserg.io) 87 | -------------------------------------------------------------------------------- /spec/method_stub_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Mock::MethodStub do 4 | describe "#method_name" do 5 | it "returns method name" do 6 | stub = Mock::MethodStub.new(:my_method) 7 | stub.method_name.should eq(:my_method) 8 | end 9 | end 10 | 11 | describe "#with" do 12 | it "has no arguments before being called" do 13 | stub = Mock::MethodStub.new(:my_method) 14 | stub.arguments.should be_nil 15 | end 16 | 17 | it "sets arguments and returns itself" do 18 | stub = Mock::MethodStub.new(:my_method) 19 | stub.with("arg1", "arg2").should eq(stub) 20 | stub.arguments.should eq(Mock::Arguments.new(["arg1", "arg2"])) 21 | end 22 | 23 | it "sets empty arguments" do 24 | stub = Mock::MethodStub.new(:my_method) 25 | stub.with().should eq(stub) 26 | stub.arguments.should eq(Mock::Arguments.empty) 27 | end 28 | 29 | it "accepts an Arguments" do 30 | arguments = Mock::Arguments.new(["arg"]) 31 | stub = Mock::MethodStub.new(:my_method).with(arguments) 32 | stub.arguments.should eq(arguments) 33 | end 34 | end 35 | 36 | describe "#and_return" do 37 | it "defaults to nil" do 38 | Mock::MethodStub.new(:my_method).value.should be_nil 39 | end 40 | 41 | it "sets value and returns itself" do 42 | stub = Mock::MethodStub.new(:my_method) 43 | stub.and_return("value").should eq(stub) 44 | stub.value.should eq("value") 45 | end 46 | end 47 | end 48 | 49 | describe Mock::MethodStubCollection do 50 | describe "#find" do 51 | it "finds when name and args are equal (value is ignored)" do 52 | coll = Mock::MethodStubCollection.new 53 | stub1 = Mock::MethodStub.new(:my_method).with("arg").and_return("value1") 54 | stub2 = Mock::MethodStub.new(:my_method).with("arg").and_return("value2") 55 | coll << stub1 56 | coll.find(stub2).should eq(stub1) 57 | coll.find(:my_method, Mock::Arguments.new(["arg"])).should eq(stub1) 58 | end 59 | 60 | it "doesn't find when names are different" do 61 | coll = Mock::MethodStubCollection.new 62 | stub1 = Mock::MethodStub.new(:my_method1).with("arg").and_return("value") 63 | stub2 = Mock::MethodStub.new(:my_method2).with("arg").and_return("value") 64 | coll << stub1 65 | coll.find(stub2).should be_nil 66 | coll.find(:my_method2, Mock::Arguments.new(["arg"])).should be_nil 67 | end 68 | 69 | it "doesn't find when arguments are different" do 70 | coll = Mock::MethodStubCollection.new 71 | stub1 = Mock::MethodStub.new(:my_method).with("arg1").and_return("value") 72 | stub2 = Mock::MethodStub.new(:my_method).with("arg2").and_return("value") 73 | coll << stub1 74 | coll.find(stub2).should be_nil 75 | coll.find(:my_method, Mock::Arguments.new(["arg2"])).should be_nil 76 | end 77 | 78 | it "no arguments == match always" do 79 | coll = Mock::MethodStubCollection.new 80 | stub1 = Mock::MethodStub.new(:my_method) 81 | stub2 = Mock::MethodStub.new(:my_method).with("arg") 82 | coll << stub1 83 | coll.find(stub2).should eq(stub1) 84 | coll.find(:my_method, Mock::Arguments.new(["arg"])).should eq(stub1) 85 | end 86 | 87 | it "no arguments == match always" do 88 | coll = Mock::MethodStubCollection.new 89 | stub1 = Mock::MethodStub.new(:my_method).with("arg") 90 | stub2 = Mock::MethodStub.new(:my_method) 91 | coll << stub1 92 | coll.find(stub2).should eq(stub1) 93 | coll.find(:my_method, Mock::Arguments.new(["arg"])).should eq(stub1) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/mock_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Mock do 4 | it "works" do 5 | my_instance = Mock::Double.new 6 | my_instance.stub(:a_method).and_return("stubbed result") 7 | 8 | my_instance.a_method.should eq("stubbed result") 9 | end 10 | 11 | it "works with parameters" do 12 | my_instance = Mock::Double.new 13 | my_instance.stub(:a_method).with(123).and_return("stubbed result") 14 | 15 | my_instance.a_method(123).should eq("stubbed result") 16 | 17 | expect_raises(Mock::UnexpectedCall) do 18 | my_instance.a_method(456) 19 | end 20 | 21 | expect_raises(Mock::UnexpectedCall) do 22 | my_instance.a_method 23 | end 24 | end 25 | 26 | it "stubbed without parameters matches always" do 27 | my_instance = Mock::Double.new 28 | my_instance.stub(:a_method).and_return("stubbed result") 29 | 30 | my_instance.a_method.should eq("stubbed result") 31 | my_instance.a_method(123).should eq("stubbed result") 32 | my_instance.a_method(456).should eq("stubbed result") 33 | end 34 | 35 | it "stubbed explicitly without parameters matches only without parameters" do 36 | my_instance = Mock::Double.new 37 | my_instance.stub(:a_method).with.and_return("stubbed result") 38 | 39 | my_instance.a_method.should eq("stubbed result") 40 | 41 | expect_raises(Mock::UnexpectedCall) do 42 | my_instance.a_method(456) 43 | end 44 | end 45 | 46 | it "can be passed as argument" do 47 | my_instance = Mock::Double.new 48 | my_instance.stub(:a_method).and_return("stubbed result") 49 | 50 | my_other_instance = MyOtherClass.new 51 | 52 | my_other_instance.a_method_with_arguments(my_instance).should eq("stubbed result") 53 | end 54 | 55 | pending "can be passed as argument (restricted)" do 56 | my_instance = Mock::Double.new 57 | my_instance.stub(:a_method).and_return("stubbed result") 58 | 59 | my_other_instance = MyOtherClass.new 60 | 61 | # this doesn't compile, need to find a workaround: 62 | my_other_instance.a_method_with_restricted_arguments(my_instance).should eq("stubbed result") 63 | end 64 | 65 | it "allows asserting method calls" do 66 | my_instance = Mock::Double.new 67 | 68 | # TODO: should[_not] receive would be better than should[_not]_receive but it's not possible to implement the way 69 | # should[_not] are implemnted in crystal. Maybe find workaround? 70 | my_instance.should_receive(:a_method).and_return("whatever") 71 | my_instance.should_not_receive(:another_method) 72 | 73 | my_instance.a_method.should eq("whatever") 74 | 75 | expect_raises(Mock::UnexpectedCall) do 76 | my_instance.another_method 77 | end 78 | 79 | expect_raises(Spec::AssertionFailed) do 80 | my_instance.stub(:another_method) 81 | my_instance.another_method 82 | my_instance.check_expectations # trick: this is what will be called later but we want to check here 83 | end 84 | 85 | Mock.reset # so it doesn't fail there 86 | end 87 | 88 | it "asserting method calls is sensitive to arguments" do 89 | my_instance = Mock::Double.new 90 | 91 | my_instance.should_receive(:a_method_with_arguments).with("hello").and_return("world") 92 | my_instance.should_not_receive(:a_method_with_arguments).with("bye") 93 | 94 | my_instance.a_method_with_arguments("hello").should eq("world") 95 | 96 | expect_raises(Spec::AssertionFailed) do 97 | my_instance.stub(:a_method_with_arguments) 98 | my_instance.a_method_with_arguments("bye") 99 | my_instance.check_expectations # trick: this is what will be called later but we want to check here 100 | end 101 | 102 | Mock.reset # so it doesn't fail there 103 | end 104 | 105 | it "asserting method calls is sensitive to arguments (trickier example)" do 106 | my_instance = Mock::Double.new 107 | 108 | my_instance.stub(:a_method_with_arguments).and_return("whatever argument") 109 | my_instance.should_receive(:a_method_with_arguments) 110 | my_instance.should_not_receive(:a_method_with_arguments).with("bye") 111 | 112 | expect_raises(Spec::AssertionFailed) do 113 | my_instance.a_method_with_arguments("bye").should eq("whatever argument") 114 | my_instance.check_expectations # trick: this is what will be called later but we want to check here 115 | end 116 | 117 | Mock.reset # so it doesn't fail there 118 | end 119 | end 120 | --------------------------------------------------------------------------------