I have a client object in a ruby gem that needs to work with a web service. I am testing to verify that it can be properly initialised and throws an error if all arguments are not passed in.
Here are my specs:
describe 'Contentstack::Client Configuration' do
describe ":access_token" do
it "is required" do
expect { create_client(access_token: nil) }.to raise_error(ArgumentError)
end
end
describe ":access_key" do
it "is required" do
expect { create_client(access_key: nil) }.to raise_error(ArgumentError)
end
end
describe ":environment" do
it "is required" do
expect { create_client(environment: nil) }.to raise_error(ArgumentError)
end
end
end
Here is the gem code:
module Contentstack
class Client
attr_reader :access_key, :access_token, :environment
def initialize(access_key:, access_token:, environment:)
#access_key = access_key
#access_token = access_token
#environment = environment
validate_configuration!
end
def validate_configuration!
fail(ArgumentError, "You must specify an access_key") if access_key.nil?
fail(ArgumentError, "You must specify an access_token") if access_token.nil?
fail(ArgumentError, "You must specify an environment") if environment.nil?
end
end
end
and here is the spec_helper method:
def create_client(access_token:, access_key:, environment:)
Contentstack::Client.new(access_token: access_token, access_key: access_key, environment: environment)
end
The problem is: I can't find a way to make these tests fail before they pass. These tests always pass because ruby throws an ArgumentError by default. I don't understand if this is the right approach to TDD. How do I get into a red-green-refactor cycle with this scenario?
create_client raises the ArgumentError, because it expects three keyword arguments and you are passing only one: (maybe you should have tested your helper, too)
def create_client(access_token:, access_key:, environment:)
# intentionally left empty
end
create_client(access_key: nil)
# in `create_client': missing keywords: access_token, environment (ArgumentError)
You could use default values in your helper to overcome this:
def create_client(access_token: :foo, access_key: :bar, environment: :baz)
Contentstack::Client.new(access_token: access_token, access_key: access_key, environment: environment)
end
create_client(access_key: nil)
# in `validate_configuration!': You must specify an access_key (ArgumentError)
Finally, you could be more specific regarding the error message:
expect { ... }.to raise_error(ArgumentError, 'You must specify an access_key')
please refer to the answer by Stefan, it’s way more proper
The proper way would be to mock Client#validate_configuration! to do nothing, but here it might be even simpler. Put in your test_helper.rb:
Client.prepend(Module.new do
def validate_configuration!; end
end)
Frankly, I do not see any reason to force tests to fail before they pass in this particular case. To follow the TDD, you should have been running tests before validate_configuration! implementation. Then those tests would have failed.
But since you have implemented it in advance, there is no need to blindly thoughtless follow the rule “test must fail before pass.”
Related
Hi I need to know how to do the following
rspec code:
2) WebServer::Htaccess#authorized? for valid-user with valid credentials returns true
Failure/Error: expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
ArgumentError:
wrong number of arguments calling `authorized?` (1 for 0)
# ./spec/lib/config/htaccess_spec.rb:82:in `(root)'
# ./spec/lib/config/htaccess_spec.rb:44:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:41:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:40:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:81:in `(root)'
Here is the spec.rb file
let(:htaccess_valid_user) { WebServer::Htaccess.new(valid_user_content) }
let(:htaccess_user) { WebServer::Htaccess.new(user_content) }
describe '#authorized?' do
context 'for valid-user' do
context 'with valid credentials' do
it 'returns true' do
stub_htpwd_file do
expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
end
end
end
context 'with invalid credentials' do
it 'returns false' do
stub_htpwd_file do
expect(htaccess_valid_user.authorized?(encrypted_string('bad user'))).not_to be_nil
expect(htaccess_valid_user.authorized?(encrypted_string('bad user'))).to be_false
end
end
end
end
I am new to ruby TDD, and all I have in my file right now is
def authorized?
end
I am fluent in Node.js but this is completely new to me.
Please help.
It's right there in the error message.
ArgumentError:
wrong number of arguments calling `authorized?` (1 for 0)
You've passed arguments to the authorized? method.
expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
^^^^^^^^^^^^^^^^^^
But authorized? takes no arguments.
def authorized?
end
Unlike Javascript, Ruby will check you passed in the right number of arguments. If you specify no argument list, the default is to enforce taking no arguments. Add some.
def authorized?(authorization)
end
I have the following code:
context 'user doesnt exists with that email' do
let(:params) { original_params.merge(login: "nouser#example.org") }
it_behaves_like '404'
it_behaves_like 'json result'
it_behaves_like 'auditable created'
end
It is dry because I can use these elements in other contexts as well:
context 'user exists with that email' do
it_behaves_like '200'
it_behaves_like 'json result'
end
My shared_example is:
...
RSpec.shared_examples "json result" do
specify 'returns JSON' do
api_call params, developer_header
expect { JSON.parse(response.body) }.not_to raise_error
end
end
...
The benefits are that the spec is more readable and is dry. The spec failure points to the shared_example file rather than the original spec. It is hard to debug.
The following error occurs at login_api_spec:25, but this is rspecs output:
rspec ./spec/support/shared_examples/common_returns.rb:14 # /api/login GET /email user doesnt exists with that email behaves like 401 returns 401
Any good advice how to proceed to write both dry and easy to debug rspec?
Without shared examples the code would be much longer and not as easy to read:
context 'user doesnt exists with that email' do
let(:params) { original_params.merge(login: "nouser#example.org") }
specify "returns 404" do
api_call params, developer_header
expect(response.status).to eq(404)
end
specify 'returns JSON' do
api_call params, developer_header
expect { JSON.parse(response.body) }.not_to raise_error
end
specify 'creates an api call audit' do
expect do
api_call params, developer_header
end.to change{ EncoreBackend::ApiCallAudit.count }.by(1)
end
end
I have thousands of RSpec tests like this so it is very beneficial to write the specs with shared examples because it is fast to write, but the debugging is hard.
Amongst the detailed errors there is description like this:
Shared Example Group: "restricted_for developers" called from ./spec/api/login_api_spec.rb:194
This tells the exact place of the error
I am running rspec tests on a catalog object from within a Ruby app, using Rspec::Core::Runner::run:
File.open('/tmp/catalog', 'w') do |out|
YAML.dump(catalog, out)
end
...
unless RSpec::Core::Runner::run(spec_dirs, $stderr, out) == 0
raise Puppet::Error, "Unit tests failed:\n#{out.string}"
end
(The full code can be found at https://github.com/camptocamp/puppet-spec/blob/master/lib/puppet/indirector/catalog/rest_spec.rb)
In order to pass the object I want to test, I dump it as YAML to a file (currently /tmp/catalog) and load it as subject in my tests:
describe 'notrun' do
subject { YAML.load_file('/tmp/catalog') }
it { should contain_package('ppet') }
end
Is there a way I could pass the catalog object as subject to my tests without dumping it to a file?
I am not very clear as to what exactly you are trying to achieve but from my understanding I feel that using a before(:each) hook might be of use to you. You can define variables in this block that are available to all the stories in that scope.
Here is an example:
require "rspec/expectations"
class Thing
def widgets
#widgets ||= []
end
end
describe Thing do
before(:each) do
#thing = Thing.new
end
describe "initialized in before(:each)" do
it "has 0 widgets" do
# #thing is available here
#thing.should have(0).widgets
end
it "can get accept new widgets" do
#thing.widgets << Object.new
end
it "does not share state across examples" do
#thing.should have(0).widgets
end
end
end
You can find more details at:
https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks/before-and-after-hooks#define-before(:each)-block
I have a line in my test:
page.has_reply?("my reply").must_equal true
and to make it more readable I want to use a custom matcher:
page.must_have_reply "my reply"
Based on the docs for https://github.com/zenspider/minitest-matchers I expect I need to write a matcher which looks something like this:
def have_reply(text)
subject.has_css?('.comment_body', :text => text)
end
MiniTest::Unit::TestCase.register_matcher :have_reply, :have_reply
The problem is that I can't see how to get a reference to the subject (i.e. the page object). The docs say "Note subject must be the first argument in assertion" but that doesn't really help.
There is a little example, you can create a class which should responds to set of methods matches?, failure_message_for_should, failure_message_for_should_not.
In matches? method you can get the reference to the subject.
class MyMatcher
def initialize(text)
#text = text
end
def matches? subject
subject =~ /^#{#text}.*/
end
def failure_message_for_should
"expected to start with #{#text}"
end
def failure_message_for_should_not
"expected not to start with #{#text}"
end
end
def start_with(text)
MyMatcher.new(text)
end
MiniTest::Unit::TestCase.register_matcher :start_with, :start_with
describe 'something' do
it 'must start with...' do
page = 'my reply'
page.must_start_with 'my reply'
page.must_start_with 'my '
end
end
There are many ways to get what you want here. The easiest way is to not mess with assertions, expectations, or matchers at all and just use an assert. So, assuming you already have the has_reply? method defined, you could just use this:
assert page.has_reply?("my reply")
But, that doesn't get you the must_have_reply syntax you are asking for. And I doubt you really have a has_reply? method. So, let's start.
Your asked "how to get a reference to the subject (i.e. the page object)". In this case the subject is the object that the must_have_reply method is defined on. So, you should use this instead of subject. But its not as straightforward as all that. Matchers add a level of indirection that we don't have with the usual Assertions (assert_equal, refute_equal) or Expectations (must_be_equal, wont_be_equal). If you want to write a Matcher you need to implement the Matcher API.
Fortunately for you you don't really have to implement the API. Since it seems you are already intending on relying on Cabybara's have_css matcher, we can simply use Capybara's HaveSelector class and let it implement the proper API. We just need to create our own Matchers module with a method that returns a HaveSelector object.
# Require Minitest Matchers to make this all work
require "minitest/matchers"
# Require Capybara's matchers so you can use them
require "capybara/rspec/matchers"
# Create your own matchers module
module YourApp
module Matchers
def have_reply text
# Return a properly configured HaveSelector instance
Capybara::RSpecMatchers::HaveSelector.new(:css, ".comment_body", :text => text)
end
# Register module using minitest-matcher syntax
def self.included base
instance_methods.each do |name|
base.register_matcher name, name
end
end
end
end
Then, in your minitest_helper.rb file, you can include your Matchers module so you can use it. (This code will include the matcher in all tests.)
class MiniTest::Rails::ActiveSupport::TestCase
# Include your module in the test case
include YourApp::Matchers
end
Minitest Matchers does all the hard lifting. You can now you can use your matcher as an assertion:
def test_using_an_assertion
visit root_path
assert_have_reply page, "my reply"
end
Or, you can use your matcher as an expectation:
it "is an expectation" do
visit root_path
page.must_have_reply "my reply"
end
And finally you can use it with a subject:
describe "with a subject" do
before { visit root_path }
subject { page }
it { must have_reply("my reply") }
must { have_reply "my reply" }
end
Important: For this to work, you must be using 'gem minitest-matchers', '>= 1.2.0' because register_matcher is not defined in earlier versions of that gem.
I have the following class:
I want to ensure the class url is only set once for all instances.
class DataFactory
##url = nil
def initialize()
begin
if ##url.nil?
Rails.logger.debug "Setting url"
##url = MY_CONFIG["my value"]
end
rescue Exception
raise DataFactoryError, "Error!"
end
end
end
I have two tests:
it "should log a message" do
APP_CONFIG = {"my value" => "test"}
Rails.stub(:logger).and_return(logger_mock)
logger_mock.should_receive(:debug).with "Setting url"
t = DataFactory.new
t = nil
end
it "should throw an exception" do
APP_CONFIG = nil
expect {
DataFactory.new
}.to raise_error(DataFactoryError, /Error!/)
end
The problem is the second test never throws an exception as the ##url class variable is still set from the first test when the second test runs.
Even though I have se the instance to nil at the end of the first test garbage collection has not cleared the memory before the second test runs:
Any ideas would be great!
I did hear you could possibly use Class.new but I am not sure how to go about this.
describe DataFactory
before(:each) { DataFactory.class_variable_set :##url, nil }
...
end
Here is an alternative to the accepted answer, which while wouldn't solve your particular example, I'm hoping it might help a few people with a question in the same vein. If the class in question doesn't specify a default value, and remains undefined until set, this seems to work:
describe DataFactory
before(:each) do
DataFactory.remove_class_variable :##url if DataFactory.class_variable_defined? :##url
end
...
end
Works for me with a class with something more like:
def initialize
##url ||= MY_CONFIG["my value"]
...
end