diff --git a/README.md b/README.md index b8bfaab..b5ef0c2 100644 --- a/README.md +++ b/README.md @@ -361,10 +361,10 @@ Items not marked as completed may have partial implementations. ### How it Works (in a nutshell) This shard makes extensive use of the Crystal macro system to build classes and modules. -Each `describe` and `context` block creates a new module nested in its parent. -The `it` block creates an example class. -An instance of the example class is created to run the test. -Each example class includes a context module, which contains all test values and hooks. +Each `describe` and `context` block creates a new class that inherits its parent. +The `it` block creates an method. +An instance of the group class is created to run the test. +Each group class includes all test values and hooks. Contributing ------------ @@ -379,11 +379,6 @@ Please make sure to run `crystal tool format` before submitting. The CI build checks for properly formatted code. [Ameba](https://crystal-ameba.github.io/) is run to check for code style. -Tests must be written for any new functionality. -Macros that create types are not as easy to test, -so they are exempt for the current time. -However, please test all code locally with an example spec file. - Documentation is automatically generated and published to GitLab pages. It can be found here: https://arctic-fox.gitlab.io/spectator @@ -391,7 +386,17 @@ This project is developed on [GitLab](https://gitlab.com/arctic-fox/spectator), and mirrored to [GitHub](https://github.com/icy-arctic-fox/spectator). Issues and PRs/MRs are accepted on both. -Contributors ------------- +### Testing -- [arctic-fox](https://gitlab.com/arctic-fox) Michael Miller - creator, maintainer +Tests must be written for any new functionality. + +The `spec/` directory contains feature tests as well as unit tests. +These demonstrate small bits of functionality. +The feature tests are grouped into sub directories based on their type, they are: + +- docs/ - Example snippets from Spectator's documentation. +- rspec/ - Examples from RSpec's documentation modified slightly to work with Spectator. + See: https://relishapp.com/rspec/ + Additional sub directories in this directory represent the modules/projects of RSpec. + +The other directories are for unit testing various parts of Spectator. diff --git a/shard.yml b/shard.yml index b118ead..15d511e 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.11 +version: 0.9.13 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/spec/docs/getting_started_spec.cr b/spec/docs/getting_started_spec.cr new file mode 100644 index 0000000..e73dda5 --- /dev/null +++ b/spec/docs/getting_started_spec.cr @@ -0,0 +1,23 @@ +require "../spec_helper" + +Spectator.describe String do + subject { "foo" } + + describe "#==" do + context "with the same value" do + let(value) { subject.dup } + + it "is true" do + is_expected.to eq(value) + end + end + + context "with a different value" do + let(value) { "bar" } + + it "is false" do + is_expected.to_not eq(value) + end + end + end +end diff --git a/spec/docs/helper_methods_spec.cr b/spec/docs/helper_methods_spec.cr new file mode 100644 index 0000000..ddfb34e --- /dev/null +++ b/spec/docs/helper_methods_spec.cr @@ -0,0 +1,57 @@ +Spectator.describe String do + # This is a helper method. + def random_string(length) + chars = ('a'..'z').to_a + String.build(length) do |builder| + length.times { builder << chars.sample } + end + end + + describe "#size" do + subject { random_string(10).size } + + it "is the length of the string" do + is_expected.to eq(10) + end + end +end +Spectator.describe String do + # length is now pulled from value defined by `let`. + def random_string + chars = ('a'..'z').to_a + String.build(length) do |builder| + length.times { builder << chars.sample } + end + end + + describe "#size" do + let(length) { 10 } # random_string uses this. + subject { random_string.size } + + it "is the length of the string" do + is_expected.to eq(length) + end + end +end + +module StringHelpers + def random_string + chars = ('a'..'z').to_a + String.build(length) do |builder| + length.times { builder << chars.sample } + end + end +end + +Spectator.describe String do + include StringHelpers + + describe "#size" do + let(length) { 10 } + subject { random_string.size } + + it "is the length of the string" do + is_expected.to eq(length) + end + end +end diff --git a/spec/docs/mocks/mocks_spec.cr b/spec/docs/mocks/mocks_spec.cr new file mode 100644 index 0000000..94e40de --- /dev/null +++ b/spec/docs/mocks/mocks_spec.cr @@ -0,0 +1,30 @@ +class Phonebook + def find(name) + # Some expensive lookup call. + "+18005554321" + end +end + +class Resolver + def initialize(@phonebook : Phonebook) + end + + def find(name) + @phonebook.find(name) + end +end + +Spectator.describe Resolver do + mock Phonebook do + stub find(name) + end + + describe "#find" do + it "can find number" do + pb = Phonebook.new + allow(pb).to receive(find).and_return("+18005551234") + resolver = Resolver.new(pb) + expect(resolver.find("Bob")).to eq("+18005551234") + end + end +end diff --git a/spec/docs/mocks/overview_spec.cr b/spec/docs/mocks/overview_spec.cr new file mode 100644 index 0000000..d5f6dfb --- /dev/null +++ b/spec/docs/mocks/overview_spec.cr @@ -0,0 +1,40 @@ +require "../../spec_helper" + +Spectator.describe "Doubles" do + double :my_double do + stub answer { 42 } + end + + specify "the answer to everything" do + dbl = double(:my_double) + expect(dbl.answer).to eq(42) + end +end + +class MyType + def answer + 123 + end +end + +Spectator.describe "Mocks" do + mock MyType do + stub answer { 42 } + end + + specify "the answer to everything" do + m = MyType.new + expect(m.answer).to eq(42) + end +end +Spectator.describe "Mocks and doubles" do + double :my_double do + stub answer : Int32 # Return type required, otherwise nil is assumed. + end + + specify "the answer to everything" do + dbl = double(:my_double) + allow(dbl).to receive(:answer).and_return(42) + expect(dbl.answer).to eq(42) + end +end diff --git a/spec/docs/mocks/stubs_spec.cr b/spec/docs/mocks/stubs_spec.cr new file mode 100644 index 0000000..75d5191 --- /dev/null +++ b/spec/docs/mocks/stubs_spec.cr @@ -0,0 +1,166 @@ +require "../../spec_helper" + +Spectator.describe "Stubs" do + context "Implementing a Stub" do + double :my_double do + stub answer : Int32 + stub do_something + end + + it "knows the answer" do + dbl = double(:my_double) + allow(dbl).to receive(:answer).and_return(42) + expect(dbl.answer).to eq(42) + end + + it "does something" do + dbl = double(:my_double) + allow(dbl).to receive(:do_something) + expect(dbl.do_something).to be_nil + end + + context "and_return" do + double :my_double do + stub to_s : String + stub do_something + end + + it "stringifies" do + dbl = double(:my_double) + allow(dbl).to receive(:to_s).and_return("foobar") + expect(dbl.to_s).to eq("foobar") + end + + it "returns gibberish" do + dbl = double(:my_double) + allow(dbl).to receive(:to_s).and_return("foo", "bar", "baz") + expect(dbl.to_s).to eq("foo") + expect(dbl.to_s).to eq("bar") + expect(dbl.to_s).to eq("baz") + expect(dbl.to_s).to eq("baz") + end + + it "returns nil" do + dbl = double(:my_double) + allow(dbl).to receive(:do_something).and_return + expect(dbl.do_something).to be_nil + end + end + + context "and_raise" do + double :my_double do + stub oops + end + + it "raises an error" do + dbl = double(:my_double) + allow(dbl).to receive(:oops).and_raise(DivisionByZeroError.new) + expect { dbl.oops }.to raise_error(DivisionByZeroError) + end + it "raises an error" do + dbl = double(:my_double) + allow(dbl).to receive(:oops).and_raise("Something broke") + expect { dbl.oops }.to raise_error(/Something broke/) + end + it "raises an error" do + dbl = double(:my_double) + allow(dbl).to receive(:oops).and_raise(ArgumentError, "Size must be > 0") + expect { dbl.oops }.to raise_error(ArgumentError, /Size/) + end + end + + context "and_call_original" do + class MyType + def foo + "foo" + end + end + + mock MyType do + stub foo + end + + it "calls the original" do + instance = MyType.new + allow(instance).to receive(:foo).and_call_original + expect(instance.foo).to eq("foo") + end + end + + context "Short-hand for Multiple Stubs" do + double :my_double do + stub method_a : Symbol + stub method_b : Int32 + stub method_c : String + end + + it "does something" do + dbl = double(:my_double) + allow(dbl).to receive_messages(method_a: :foo, method_b: 42, method_c: "foobar") + expect(dbl.method_a).to eq(:foo) + expect(dbl.method_b).to eq(42) + expect(dbl.method_c).to eq("foobar") + end + end + + context "Custom Implementation" do + double :my_double do + stub foo : String + end + + it "does something" do + dbl = double(:my_double) + allow(dbl).to receive(:foo) { "foo" } + expect(dbl.foo).to eq("foo") + end + end + + context "Arguments" do + double :my_double do + stub add(a, b) { a + b } + stub do_something(arg) { arg } # Return the argument by default. + end + + it "adds two numbers" do + dbl = double(:my_double) + allow(dbl).to receive(:add).and_return(7) + expect(dbl.add(1, 2)).to eq(7) + end + + it "does basic matching" do + dbl = double(:my_double) + allow(dbl).to receive(:do_something).with(1).and_return(42) + allow(dbl).to receive(:do_something).with(2).and_return(22) + expect(dbl.do_something(1)).to eq(42) + expect(dbl.do_something(2)).to eq(22) + end + + it "can call the original" do + dbl = double(:my_double) + allow(dbl).to receive(:do_something).with(1).and_return(42) + allow(dbl).to receive(:do_something).with(2).and_call_original + expect(dbl.do_something(1)).to eq(42) + expect(dbl.do_something(2)).to eq(2) + end + + it "falls back to the default" do + dbl = double(:my_double) + allow(dbl).to receive(:do_something).and_return(22) + allow(dbl).to receive(:do_something).with(1).and_return(42) + expect(dbl.do_something(1)).to eq(42) + expect(dbl.do_something(2)).to eq(22) + expect(dbl.do_something(3)).to eq(22) + end + + it "does advanced matching" do + dbl = double(:my_double) + allow(dbl).to receive(:do_something).with(Int32).and_return(42) + allow(dbl).to receive(:do_something).with(String).and_return("foobar") + allow(dbl).to receive(:do_something).with(/hi/).and_return("hello there") + expect(dbl.do_something(1)).to eq(42) + expect(dbl.do_something("foo")).to eq("foobar") + expect(dbl.do_something("hi there")).to eq("hello there") + end + end + end +end diff --git a/spec/docs/structure_spec.cr b/spec/docs/structure_spec.cr new file mode 100644 index 0000000..d007bf1 --- /dev/null +++ b/spec/docs/structure_spec.cr @@ -0,0 +1,34 @@ +require "../spec_helper" + +Spectator.describe String do + let(normal_string) { "foobar" } + let(empty_string) { "" } + + describe "#empty?" do + subject { string.empty? } + + context "when empty" do + let(string) { empty_string } + + it "is true" do + is_expected.to be_true + end + end + + context "when not empty" do + let(string) { normal_string } + + it "is false" do + is_expected.to be_false + end + end + end +end + +Spectator.describe Bytes do + it "stores an array of bytes" do + bytes = Bytes.new(32) + bytes[0] = 42 + expect(bytes[0]).to eq(42) + end +end diff --git a/spec/docs/subject_spec.cr b/spec/docs/subject_spec.cr new file mode 100644 index 0000000..459da35 --- /dev/null +++ b/spec/docs/subject_spec.cr @@ -0,0 +1,32 @@ +require "../spec_helper" + +Spectator.describe "subject" do + subject(array1) { [1, 2, 3] } + subject(array2) { [4, 5, 6] } + + it "has different elements" do + expect(array1).to_not eq(subject) # array2 would also work here. + end + + let(string) { "foobar" } + + it "isn't empty" do + expect(string.empty?).to be_false + end + + it "is six characters" do + expect(string.size).to eq(6) + end + + let(array) { [0, 1, 2] } + + it "modifies the array" do + array[0] = 42 + expect(array).to eq([42, 1, 2]) + end + + it "doesn't carry across tests" do + array[1] = 777 + expect(array).to eq([0, 777, 2]) + end +end diff --git a/src/spectator.cr b/src/spectator.cr index f1248ec..32e99f1 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.11" + VERSION = "0.9.13" # Top-level describe method. # All specs in a file must be wrapped in this call. diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index ca28a4a..cc2d373 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -50,12 +50,9 @@ module Spectator::Mocks %} {% if body && !body.is_a?(Nop) %} - %source = ::Spectator::Source.new({{_file}}, {{_line}}) - %proc = ->{ + def {{receiver}}%method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} {{body.body}} - } - %ds = ::Spectator::Mocks::ProcMethodStub.new({{name.symbolize}}, %source, %proc) - ::Spectator::SpecBuilder.add_default_stub({{@type.name}}, %ds) + end {% end %} def {{receiver}}{{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} @@ -66,8 +63,15 @@ module Spectator::Mocks if (%stub = %harness.mocks.find_stub(self, %call)) return %stub.call!(%args) { {{original}}({{args.splat}}) } end + + {% if body && !body.is_a?(Nop) %} + %method({{args.splat}}) + {% else %} + {{original}}({{args.splat}}) + {% end %} + else + {{original}}({{args.splat}}) end - {{original}}({{args.splat}}) end def {{receiver}}{{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} @@ -78,9 +82,18 @@ module Spectator::Mocks if (%stub = %harness.mocks.find_stub(self, %call)) return %stub.call!(%args) { {{original}}({{args.splat}}) { |*%ya| yield *%ya } } end - end - {{original}}({{args.splat}}) do |*%yield_args| - yield *%yield_args + + {% if body && !body.is_a?(Nop) %} + %method({{args.splat}}) { {{original}}({{args.splat}}) { |*%ya| yield *%ya } } + {% else %} + {{original}}({{args.splat}}) do |*%yield_args| + yield *%yield_args + end + {% end %} + else + {{original}}({{args.splat}}) do |*%yield_args| + yield *%yield_args + end end end end diff --git a/util/mirror-wiki.sh b/util/mirror-wiki.sh new file mode 100755 index 0000000..684d545 --- /dev/null +++ b/util/mirror-wiki.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -ex + +# Mirrors the contents of the GitLab wiki to GitHub. +git clone git@gitlab.com:arctic-fox/spectator.wiki.git +pushd spectator.wiki +git remote add github git@github.com:icy-arctic-fox/spectator.wiki.git +git fetch github +git push github master +popd +rm -rf spectator.wiki