bogus 0.0.2 → 0.0.3.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile.lock +14 -2
  3. data/Guardfile +5 -0
  4. data/README.md +7 -249
  5. data/bogus.gemspec +3 -1
  6. data/features/.nav +21 -0
  7. data/features/authors.md +42 -0
  8. data/{CHANGELOG.md → features/changelog.md} +9 -2
  9. data/features/configuration_options.feature +1 -2
  10. data/features/{contract_tests_mocks.feature → contract_tests/contract_tests_mocks.feature} +4 -1
  11. data/features/{contract_tests_spies.feature → contract_tests/contract_tests_spies.feature} +2 -0
  12. data/features/{contract_tests_stubs.feature → contract_tests/contract_tests_stubs.feature} +14 -0
  13. data/features/contract_tests/readme.md +26 -0
  14. data/features/{return_value_contracts.feature → contract_tests/return_value_contracts.feature} +6 -2
  15. data/features/{anonymous_doubles.feature → fakes/anonymous_doubles.feature} +24 -8
  16. data/features/{fake_objects.feature → fakes/fake_objects.feature} +39 -4
  17. data/features/fakes/global_fake_configuration.feature +88 -0
  18. data/features/fakes/readme.md +11 -0
  19. data/features/fakes/replacing_classes.feature +148 -0
  20. data/features/getting_started.md +36 -0
  21. data/features/license.md +9 -0
  22. data/features/readme.md +173 -0
  23. data/features/safe_stubbing/argument_matchers.feature +30 -0
  24. data/features/safe_stubbing/readme.md +35 -0
  25. data/features/{safe_mocking.feature → safe_stubbing/safe_mocking.feature} +14 -0
  26. data/features/{safe_stubbing.feature → safe_stubbing/safe_stubbing.feature} +18 -4
  27. data/features/{spies.feature → safe_stubbing/spies.feature} +19 -4
  28. data/features/step_definitions/rspec_steps.rb +14 -5
  29. data/features/support/env.rb +4 -0
  30. data/lib/bogus.rb +2 -0
  31. data/lib/bogus/adds_recording.rb +11 -7
  32. data/lib/bogus/any_args.rb +7 -0
  33. data/lib/bogus/anything.rb +11 -0
  34. data/lib/bogus/contract_not_fulfilled.rb +17 -10
  35. data/lib/bogus/copies_classes.rb +2 -6
  36. data/lib/bogus/creates_fakes_with_stubbed_methods.rb +40 -0
  37. data/lib/bogus/double.rb +28 -8
  38. data/lib/bogus/ensures_all_interactions_satisfied.rb +36 -0
  39. data/lib/bogus/fake.rb +6 -0
  40. data/lib/bogus/fake_configuration.rb +69 -0
  41. data/lib/bogus/fakes_classes.rb +21 -0
  42. data/lib/bogus/has_overwritten_methods.rb +24 -0
  43. data/lib/bogus/have_received_matcher.rb +63 -0
  44. data/lib/bogus/injector.rb +36 -14
  45. data/lib/bogus/interaction.rb +33 -17
  46. data/lib/bogus/makes_substitute_methods.rb +15 -0
  47. data/lib/bogus/mocking_dsl.rb +31 -0
  48. data/lib/bogus/multi_stubber.rb +15 -0
  49. data/lib/bogus/not_all_expectations_satisfied.rb +27 -0
  50. data/lib/bogus/overwriten_classes.rb +15 -0
  51. data/lib/bogus/overwrites_classes.rb +2 -2
  52. data/lib/bogus/overwrites_methods.rb +42 -0
  53. data/lib/bogus/proxies_method_calls.rb +23 -0
  54. data/lib/bogus/proxy_class.rb +25 -16
  55. data/lib/bogus/public_methods.rb +36 -11
  56. data/lib/bogus/record_interactions.rb +3 -9
  57. data/lib/bogus/recording_proxy.rb +5 -0
  58. data/lib/bogus/registers_created_fakes.rb +2 -1
  59. data/lib/bogus/resets_overwritten_classes.rb +14 -0
  60. data/lib/bogus/resets_stubbed_methods.rb +12 -0
  61. data/lib/bogus/responds_to_everything.rb +11 -0
  62. data/lib/bogus/rspec.rb +7 -0
  63. data/lib/bogus/rspec_adapter.rb +17 -0
  64. data/lib/bogus/rspec_extensions.rb +8 -20
  65. data/lib/bogus/shadow.rb +60 -0
  66. data/lib/bogus/verifies_contracts.rb +5 -1
  67. data/lib/bogus/verifies_stub_definition.rb +5 -0
  68. data/lib/bogus/version.rb +1 -1
  69. data/lib/tracks_existence_of_test_doubles.rb +11 -0
  70. data/spec/bogus/adds_recording_spec.rb +46 -10
  71. data/spec/bogus/anything_spec.rb +13 -0
  72. data/spec/bogus/copies_classes_spec.rb +4 -3
  73. data/spec/bogus/creates_fakes_with_stubbed_methods_spec.rb +121 -0
  74. data/spec/bogus/double_spec.rb +63 -20
  75. data/spec/bogus/ensures_all_interactions_satisfied_spec.rb +43 -0
  76. data/spec/bogus/fake_configuration_spec.rb +99 -0
  77. data/spec/bogus/fakes_classes_spec.rb +46 -0
  78. data/spec/bogus/have_received_matcher_spec.rb +56 -0
  79. data/spec/bogus/interaction_spec.rb +18 -7
  80. data/spec/bogus/interactions_repository_spec.rb +42 -0
  81. data/spec/bogus/makes_substitute_methods_spec.rb +24 -0
  82. data/spec/bogus/mocking_dsl_spec.rb +234 -7
  83. data/spec/bogus/multi_stubber_spec.rb +31 -0
  84. data/spec/bogus/overwriten_classes_spec.rb +27 -0
  85. data/spec/bogus/overwrites_classes_spec.rb +2 -2
  86. data/spec/bogus/overwrites_methods_spec.rb +107 -0
  87. data/spec/bogus/proxy_class_spec.rb +6 -0
  88. data/spec/bogus/record_interactions_spec.rb +3 -4
  89. data/spec/bogus/registers_created_fakes_spec.rb +8 -0
  90. data/spec/bogus/resets_overwritten_classes_spec.rb +26 -0
  91. data/spec/bogus/resets_stubbed_methods_spec.rb +16 -0
  92. data/spec/bogus/shadow_spec.rb +182 -0
  93. data/spec/bogus/verifies_contracts_spec.rb +9 -3
  94. data/spec/bogus/verifies_stub_definition_spec.rb +4 -0
  95. data/spec/spec_helper.rb +3 -0
  96. data/spec/support/fake_creator_of_fakes.rb +15 -0
  97. data/spec/support/sample_fake.rb +13 -0
  98. data/spec/tracks_existence_of_test_doubles_spec.rb +26 -0
  99. metadata +105 -32
  100. data/lib/bogus/creates_anonymous_stubs.rb +0 -27
  101. data/lib/bogus/invocation_matcher.rb +0 -27
  102. data/lib/bogus/rr_proxy.rb +0 -5
  103. data/spec/bogus/invocation_matcher_spec.rb +0 -26
@@ -1,7 +1,6 @@
1
1
  Feature: Configuration Options
2
2
 
3
- Bogus can be configured, similarly to many other frameworks with a configure block.
4
- This feature describes the configuration options available.
3
+ Bogus can be configured, similarly to many other frameworks with a configure block. This feature describes the configuration options available.
5
4
 
6
5
  Scenario: search_modules
7
6
  Given a file named "foo.rb" with:
@@ -1,5 +1,7 @@
1
1
  Feature: Contract tests with mocks
2
2
 
3
+ Whenever you mock something, you specify a contract on the arguments/return value pair. Bogus can check automatically Whether this contract was satisfied.
4
+
3
5
  Background:
4
6
  Given a file named "foo.rb" with:
5
7
  """ruby
@@ -45,9 +47,10 @@ Feature: Contract tests with mocks
45
47
  it "does not check out the book from library if not available" do
46
48
  student = Student.new
47
49
  mock(library).has_book?("Moby Dick") { false }
48
- dont_allow(library).checkout("Moby Dick")
49
50
 
50
51
  student.read("Moby Dick", library)
52
+
53
+ library.should_not have_received.checkout("Moby Dick")
51
54
  end
52
55
  end
53
56
  """
@@ -1,5 +1,7 @@
1
1
  Feature: Contract tests with spies
2
2
 
3
+ Whenever you spy on method invocations, it creates a contract specifying that some method can be called with a particular set of arguments. It can be automatically verified by Bogus, whether the method was actually called with those arguments.
4
+
3
5
  Background:
4
6
  Given a file named "foo.rb" with:
5
7
  """ruby
@@ -1,5 +1,19 @@
1
1
  Feature: Contract tests with stubs
2
2
 
3
+ Whenever you stub any method, a contract is specified on the input/output values of that method.
4
+
5
+ When stubbing using the short syntax:
6
+
7
+ fake(:fake_name, method_name: :return_value)
8
+
9
+ the contract can only be specified on the return value.
10
+
11
+ The longer syntax:
12
+
13
+ stub(fake).method_name(args) { :return_value }
14
+
15
+ will also create a contract on the method input parameters.
16
+
3
17
  Background:
4
18
  Given a file named "foo.rb" with:
5
19
  """ruby
@@ -0,0 +1,26 @@
1
+ Whenever you write test code like this:
2
+
3
+ mock(library).checkout("Moby Dick") { raise NoSuchBookError }
4
+
5
+ There are some assumptions this code makes:
6
+
7
+ 1. There is an object in my system that can play the role of a library.
8
+ 2. The library object has a `#checkout` method that takes one argument.
9
+ 3. The system under test is supposed to call `#checkout` with argument `"Moby Dick"` at least once.
10
+ 4. There is some context in which, given argument "Moby Dick", the `#checkout` method raises `NoSuchBookError`.
11
+
12
+ While using fakes makes sure that the assumptions 1 and 2 are satisfied, and assumption number 3 is verified by the mocking system, in order to make sure that the assumption no 4 is also true, you need to write a test for the library object.
13
+
14
+ Bogus will not be able to write that test for you, but it can remind you that you should do so.
15
+
16
+ Whenever you use named fakes:
17
+
18
+ fake(:library)
19
+
20
+ Bogus will remember any interactions set up on that fake.
21
+
22
+ If you want to verify that you remembered to test all the scenarios specified by stubbing/spying/mocking on the fake object, you can put the following code in the tests for "the real thing" (i.e. the Library class in our example):
23
+
24
+ verify_contract(:library)
25
+
26
+ This will record all of the interactions you make with that class and make the tests fail if you forget to test some scenario that you recorded using a fake object.
@@ -1,5 +1,9 @@
1
1
  Feature: Return value contracts
2
2
 
3
+ In order to be able to record the stubbed value, Bogus requires that the block you pass when stubbing is free of side effects and idempotent.
4
+
5
+ If it's not, the behavior of contract verification is not defined.
6
+
3
7
  Background:
4
8
  Given a file named "foo.rb" with:
5
9
  """ruby
@@ -51,7 +55,7 @@ Feature: Return value contracts
51
55
  end
52
56
  """
53
57
 
54
- Scenario: Fails when returned values differ
58
+ Scenario: Bogus makes sure that all the return values recorded by stubbing are also present in tests of the real object
55
59
  Then spec file with following content should fail:
56
60
  """ruby
57
61
  describe AuthenticationService do
@@ -66,7 +70,7 @@ Feature: Return value contracts
66
70
  end
67
71
  """
68
72
 
69
- Scenario: Passes when interfaces match
73
+ Scenario: Bogus does not fail the tests if all the recorded values have been also recorded on the real object
70
74
  Then spec file with following content should pass:
71
75
  """ruby
72
76
  describe AuthenticationService do
@@ -1,14 +1,21 @@
1
1
  Feature: Anonymous test doubles
2
2
 
3
- Anonymous test doubles can be useful as a stepping stone towards
4
- actual fakes and when migrating from another testing library.
3
+ Anonymous test doubles can be useful as a stepping stone towards actual fakes and when migrating from another testing library.
5
4
 
6
- In contrast with other testing libraries, Bogus makes it's fakes
7
- respond to all methods by default and makes those calls chainable.
8
- This way you can spy on methods without stubbing them first.
5
+ In contrast with other testing libraries, Bogus makes it's fakes respond to all methods by default and makes those calls chainable. This way you can spy on methods without stubbing them first.
9
6
 
10
- It is not advisable to use those for anything else than an intermediate step.
11
- Fakes that mimic an actual class have many more benefits.
7
+ It is not advisable to use those for anything else than an intermediate step. Fakes that mimic an actual class have many more benefits.
8
+
9
+ The syntax for defining fakes is:
10
+
11
+ fake(method_1: return_value, method_2: proc{return_value2})
12
+
13
+ If you pass a proc as a return value to a fake, the proc will be called to obtain the value. This can be used for instance to raise errors in stubbed methods.
14
+
15
+ If you want to actually return a proc from a method, you need to use a slightly longer syntax:
16
+
17
+ factory = fake()
18
+ stub(factory).make_validator{ proc{ false } }
12
19
 
13
20
  Background:
14
21
  Given a file named "foo.rb" with:
@@ -34,7 +41,7 @@ Feature: Anonymous test doubles
34
41
  let(:jake) { Student.new("Jake") }
35
42
 
36
43
  it "allows stubbing any method with any parameters" do
37
- stub(library).register_junior { "the card" }
44
+ stub(library).register_junior(any_args) { "the card" }
38
45
 
39
46
  jake.sign_up(library)
40
47
 
@@ -58,6 +65,15 @@ Feature: Anonymous test doubles
58
65
  end
59
66
  """
60
67
 
68
+ Scenario: Stubbing methods inline by passing a block
69
+ Then the following test should pass:
70
+ """ruby
71
+ library = fake(register_junior: proc{ raise "library full!" })
72
+ expect {
73
+ library.register_junior("Jake")
74
+ }.to raise_error("library full!")
75
+ """
76
+
61
77
  Scenario: Mocking any method with any parameters
62
78
  Then spec file with following content should pass:
63
79
  """ruby
@@ -1,7 +1,29 @@
1
1
  Feature: Faking existing classes
2
2
 
3
- Bogus makes it easy to create fakes, which behave like a null-object and
4
- have the same interface as the object being faked.
3
+ You create a fake by calling the `fake` method:
4
+
5
+ fake(fake_name, options) { ClassToCopy }
6
+
7
+ If you omit the fake_name, you will get an anonymous fake, otherwise the name will be used to identify the fake for the purposes of contract tests. If you omit the block returning the class to copy, fake name will also be used to guess that.
8
+
9
+ Usually you will want the fake to return an instance of the copied class. Otherwise, you can pass `:as => :class` option to copy the class and return the copy, not an instance of it.
10
+
11
+ Options can also include methods to stub on the returned fake, which makes:
12
+
13
+ fake(:foo, bar: "value")
14
+
15
+ Equivalent to:
16
+
17
+ foo = fake(:foo)
18
+ stub(foo).bar(any_args) { "value" }
19
+
20
+ For your convenience, Bogus also defines the following macro:
21
+
22
+ fake(:foo, bar: "value")
23
+
24
+ Which is equivalent to:
25
+
26
+ let(:foo) [ fake(:foo, bar: "value")]
5
27
 
6
28
  Background:
7
29
  Given a file named "foo.rb" with:
@@ -90,5 +112,18 @@ Feature: Faking existing classes
90
112
  end
91
113
  """
92
114
 
93
- Scenario: Fakes which are objects
94
- Given pending
115
+ Scenario: Fakes with inline return values
116
+ Then spec file with following content should pass:
117
+ """ruby
118
+ describe "library class fake" do
119
+ let(:library) { fake(:library, checkout: "checked out",
120
+ return_book: "returned") }
121
+
122
+ it "sets the default return value for provided functions" do
123
+ library.checkout("Moby Dick").should == "checked out"
124
+ library.checkout("Three Musketeers").should == "checked out"
125
+ library.return_book("Moby Dick").should == "returned"
126
+ library.return_book("Three Musketeers").should == "returned"
127
+ end
128
+ end
129
+ """
@@ -0,0 +1,88 @@
1
+ Feature: Global fake configuration
2
+
3
+ In an ideal world, all our fakes would follow the tell-don't-ask principle, which would eliminate the need for stubbing, and would be instances of classes that match the fake name, which would eliminate the need for configuration of things like (`as: :class` / `as: :instance`).
4
+
5
+ However, in reality we often need to add this kind of configuration to our fake definitions, and the more collaborators a fake has, the more duplication we introduce this way.
6
+
7
+ To eliminate this duplication, Bogus comes with a DSL to configure the fakes in one place, and unify their use in all your tests.
8
+
9
+ To globally configure your fakes, all you need to do is to place code like this:
10
+
11
+ Bogus.fakes do
12
+ fake(:fake_name, as: :class, class: proc{SomeClass}) do
13
+ method_1 { return_value_1 }
14
+ method_2 return_value_2
15
+ end
16
+ end
17
+
18
+ in your spec helper, or a file required from it.
19
+
20
+ Background:
21
+ Given a file named "foo.rb" with:
22
+ """ruby
23
+ class PublicLibrary
24
+ def self.books_by_author(name)
25
+ end
26
+ end
27
+ """
28
+
29
+ Given a file named "fakes.rb" with:
30
+ """ruby
31
+ Bogus.fakes do
32
+ fake(:library, as: :class, class: proc{PublicLibrary}) do
33
+ books_by_author []
34
+ end
35
+ end
36
+ """
37
+
38
+ Scenario: Globally configured fakes have all the properties configured
39
+ Then spec file with following content should pass:
40
+ """ruby
41
+ require_relative "fakes"
42
+
43
+ describe "The library fake" do
44
+ fake(:library)
45
+
46
+ it "is a class" do
47
+ # because of the as: :class specified in the fake definition
48
+ library.should be_an_instance_of(Class)
49
+ end
50
+
51
+ it "is a copy of PublicLibrary" do
52
+ # because of the block passed into configuration
53
+ library.name.should == "PublicLibrary"
54
+ end
55
+
56
+ it "returns has stubbed books_by_author" do
57
+ # because of the inline-stubbed books_by_author
58
+ library.books_by_author("Mark Twain").should == []
59
+ end
60
+ end
61
+ """
62
+
63
+ Scenario: Overwriting stubbed methods using fake macro
64
+ Then spec file with following content should pass:
65
+ """ruby
66
+ require_relative "fakes"
67
+
68
+ describe "The library fake" do
69
+ fake(:library, books_by_author: ["Some Book"])
70
+
71
+ it "can be overridden in the shortcut definition" do
72
+ library.books_by_author("Charles Dickens").should == ["Some Book"]
73
+ end
74
+ end
75
+ """
76
+
77
+ Scenario: Overwriting stubbed methods using fake helper function
78
+ Then spec file with following content should pass:
79
+ """ruby
80
+ require_relative "fakes"
81
+
82
+ describe "The library fake" do
83
+ it "can be overridden in the helper" do
84
+ library = fake(:library, books_by_author: ["Some Book"])
85
+ library.books_by_author("Charles Dickens").should == ["Some Book"]
86
+ end
87
+ end
88
+ """
@@ -0,0 +1,11 @@
1
+ Fakes in Bogus are essentially lightweight objects that mimic the original object's interface.
2
+
3
+ Let's say that we have a `Library` class that is used to manage books and a `Student`, who can interact with the `Library` in some way.
4
+
5
+ In order to test the `Student` in isolation, we need to replace the `Library`, with some test double. Typically, you would do that by creating an anonymous stub/mock object and stubbing the required methods on it.
6
+
7
+ Using those stubs, you specify the desired interface of the library object.
8
+
9
+ The problems with that approach start when you change the `Library` class. For example, you could rename the `#checkout` method to `#checkout_book`. If you used the standard approach, where your stubs are not connected in any way to the real implementation, your tests will keep happily passing, even though the collaborator interface just changed.
10
+
11
+ Bogus saves you from this problem, because your fakes have the exact same interface as the real collaborators, so whenever you change the collaborator, but not the tested object, you will get an exception in your tests.
@@ -0,0 +1,148 @@
1
+ Feature: Replacing classes with fakes
2
+
3
+ Bogus is an opinionated piece of software. One of the opinions we have is that you should use dependency injection to make your code more modular and your classes easier to compose. However, we respect the fact, that this is currently not very popular among Ruby developers.
4
+
5
+ In order to make life easier for people who choose not to use Dependency Injection, Bogus makes it convenient to replace chosen classes in your tests with fakes.
6
+
7
+ All you need to do, is put the following code in your describe:
8
+
9
+ fake_class(FooBar, foo: "bar")
10
+
11
+ Which is a shortcut for:
12
+
13
+ before do
14
+ fake_class(FooBar, foo: "bar")
15
+ end
16
+
17
+ Background:
18
+ Given a file named "app.rb" with:
19
+ """ruby
20
+ require "yaml"
21
+
22
+ class Library
23
+ FILE = "library.yml"
24
+
25
+ def self.books
26
+ YAML.load_file(FILE)
27
+ end
28
+ end
29
+
30
+ class BookIndex
31
+ def self.by_author(author)
32
+ Library.books.select{|book| book[:author] == author}
33
+ end
34
+ end
35
+ """
36
+
37
+ And a file named "spec_helper.rb" with:
38
+ """ruby
39
+ require 'bogus/rspec'
40
+
41
+ require_relative 'app'
42
+ """
43
+
44
+ Scenario: Replacing classes and contracts
45
+ Given a file named "library_spec.rb" with:
46
+ """ruby
47
+ require_relative 'spec_helper'
48
+ require 'fileutils'
49
+
50
+ describe Library do
51
+ verify_contract(:library)
52
+
53
+ it "reads the books from the yaml file" do
54
+ books = [{name: "Tom Sawyer", author: "Mark Twain"},
55
+ {name: "Moby Dick", author: "Herman Melville"}]
56
+ File.open(Library::FILE, "w") { |f| f.print books.to_yaml }
57
+
58
+ Library.books.should == books
59
+ end
60
+
61
+ after do
62
+ FileUtils.rm_rf(Library::FILE)
63
+ end
64
+ end
65
+ """
66
+
67
+ And a file named "book_index_spec.rb" with:
68
+ """ruby
69
+ require_relative 'spec_helper'
70
+
71
+ describe BookIndex do
72
+ verify_contract(:book_index)
73
+
74
+ it "returns books written by author" do
75
+ tom_sawyer = {name: "Tom Sawyer", author: "Mark Twain"}
76
+ moby_dick = {name: "Moby Dick", author: "Herman Melville"}
77
+
78
+ fake_class(Library, books: [tom_sawyer, moby_dick])
79
+
80
+ BookIndex.by_author("Mark Twain").should == [tom_sawyer]
81
+ end
82
+ end
83
+ """
84
+
85
+ When I run `rspec book_index_spec.rb library_spec.rb`
86
+ Then all the specs should pass
87
+
88
+ Scenario: Replacing classes and contracts with a different fake name
89
+ Given a file named "library_spec.rb" with:
90
+ """ruby
91
+ require_relative 'spec_helper'
92
+ require 'fileutils'
93
+
94
+ describe Library do
95
+ verify_contract(:book_repository)
96
+
97
+ it "reads the books from the yaml file" do
98
+ books = [{name: "Tom Sawyer", author: "Mark Twain"},
99
+ {name: "Moby Dick", author: "Herman Melville"}]
100
+ File.open(Library::FILE, "w") { |f| f.print books.to_yaml }
101
+
102
+ Library.books.should == books
103
+ end
104
+
105
+ after do
106
+ FileUtils.rm_rf(Library::FILE)
107
+ end
108
+ end
109
+ """
110
+
111
+ And a file named "book_index_spec.rb" with:
112
+ """ruby
113
+ require_relative 'spec_helper'
114
+
115
+ describe BookIndex do
116
+ verify_contract(:book_index)
117
+
118
+ it "returns books written by author" do
119
+ tom_sawyer = {name: "Tom Sawyer", author: "Mark Twain"}
120
+ moby_dick = {name: "Moby Dick", author: "Herman Melville"}
121
+
122
+ fake_class(Library, fake_name: :book_repository,
123
+ books: [tom_sawyer, moby_dick])
124
+
125
+ BookIndex.by_author("Mark Twain").should == [tom_sawyer]
126
+ end
127
+ end
128
+ """
129
+
130
+ When I run `rspec book_index_spec.rb library_spec.rb`
131
+ Then all the specs should pass
132
+
133
+ Scenario: Replacing classes with a macro
134
+ Given a file named "book_index_spec.rb" with:
135
+ """ruby
136
+ require_relative 'spec_helper'
137
+
138
+ describe BookIndex do
139
+ fake_class(Library, books: [])
140
+
141
+ it "returns books written by author" do
142
+ BookIndex.by_author("Mark Twain").should == []
143
+ end
144
+ end
145
+ """
146
+
147
+ When I run `rspec book_index_spec.rb`
148
+ Then all the specs should pass