reform icon indicating copy to clipboard operation
reform copied to clipboard

reform matchers

Open musaffa opened this issue 10 years ago • 37 comments

I'm planning to port shoulda matchers to reform. Should it to be integrated into reform or you want a separate gem like reform-matchers?

musaffa avatar Oct 29 '14 05:10 musaffa

Now this is funny, because I talked about matchers with @fernandes today.

I personally don't understand what these helpers are good for? Are they meant to test "basic" validators, only? I'm a bit confused, because using matchers doesn't test my form behaviour but only its configuration. Doesn't it make more sense to test something along

form.validate({})
form.errors.to_s.must_equal ".."

In this test, I know that my form works while with matchers I can't be sure that this is the case. It becomes even more complicated when you have more complex validators that might interfere with each other - how do you test that with matchers? Or do you have to write real tests, then?

I totally can see how they help to assert "simple" validations without a million lines of test code, but ..... how do you integrate that with real tests? Please, enlighten me, guys!

apotonick avatar Oct 29 '14 08:10 apotonick

The most parts of a model are declarative in nature. I use shoulda matchers to test those declarations and i write normal tests for other parts. Since a form object is mostly declarative, it makes sense to have matchers to test those declarations.

Matchers can be real unit tests. For example, presence validator matcher sends empty data for an attribute and expect it to be invalid. It will also be possible to make it expect for an error massage (if you really want them).

musaffa avatar Oct 29 '14 10:10 musaffa

It would be great if those matchers were real unit tests - what do you think?

apotonick avatar Oct 29 '14 10:10 apotonick

I think shoulda matchers are already real unit tests. For example, when it tests for a validation, it sends valid and invalid data internally to check the validity of the model object. Shoulda matchers also support what message to expect if a validation fails. Just have a look at the source of ensure_length_of_matcher.

musaffa avatar Oct 29 '14 11:10 musaffa

@apotonick today I checked the should-matchers deeper than yesterday and yes, it runs validations

I think in this case we need to think should-matchers port not as a silver bullet, it will make tests easier for validates presence, length, numerically and basic stuff... for some complex cases, we need to write our own, like we do in pure AR

@musaffa great initiative, IMHO this "port" can help very much, if we are DRYing forms, views, business logic, why not tests? :+1:

If you wanna, do the kickstart and I help as I can

fernandes avatar Oct 29 '14 12:10 fernandes

Ok, if these matchers/macros do actual unit tests I'm delighted to use them myself in my code! I thought they were just doing some reflections on the model/form.

apotonick avatar Oct 29 '14 21:10 apotonick

@fernandes I will let you know. thanks! @apotonick my initial question is still left unanswered.

musaffa avatar Oct 30 '14 05:10 musaffa

Whoops, sorry @musaffa! :smile: I vote for reform-matchers so you can be King of the Castle! Thanks so much buddy.

How are you gonna implement the matchers? Real unit tests that call form.validate(..) and form.errors?

apotonick avatar Oct 30 '14 22:10 apotonick

I will try. :)

musaffa avatar Oct 31 '14 06:10 musaffa

In case you are interested, I have made a Polymorphic Constraints gem which enforces database constraints on polymorphic relations using triggers. It supports sqlite, postgresql and mysql.

Any suggestion will be highly welcome.

musaffa avatar Oct 31 '14 07:10 musaffa

That is pretty cool, man! Any chance we could port this on a higher level, so we can use those constraints in the form? I don't want the DB to know any business logic in my architecture. What's your thoughts on this?

Anyway, the gem looks great, I'm impressed!!! :+1:

apotonick avatar Nov 02 '14 09:11 apotonick

It would be pretty straight forward to implement this in the application code but data integrity cannot be ensured. I wanted to mimic foreign key constraint for polymorphic relations so that data integrity is maintained at the database level.

musaffa avatar Nov 02 '14 09:11 musaffa

@musaffa what is the status of your gem? If you already started, I can help with development.

vadimvorotilov avatar Nov 19 '14 11:11 vadimvorotilov

@fantgeass I've been busy with another project. I'll start soon. :)

musaffa avatar Nov 19 '14 11:11 musaffa

I've been using shoulda matchers with trailblazer/reform/rspec like this:

  describe "the contract" do
    subject { Product::Create.present(params).contract }

    it { is_expected.to validate_presence_of :name }
    it { is_expected.to validate_presence_of :weight }
    it { is_expected.to validate_presence_of :width }
    it { is_expected.to validate_presence_of :height }
    it { is_expected.to validate_presence_of :length }

    it { is_expected.to validate_numericality_of(:weight).is_greater_than(0) }
    it { is_expected.to validate_numericality_of(:width).is_greater_than(0)  }
    it { is_expected.to validate_numericality_of(:height).is_greater_than(0) }
    it { is_expected.to validate_numericality_of(:length).is_greater_than(0) }
  end

Works fine.

zavan avatar Dec 04 '14 23:12 zavan

so it works out of the box. cool!

musaffa avatar Dec 05 '14 00:12 musaffa

@musaffa Yeah, it seems to, but I haven't stumbled in any edge cases yet.

Right now I'm building a form with a nested collection, something like:

class OrderForm < Reform::Form
  property :number,      validates: {presence: true}
  property :customer_id, validates: {presence: true}

  collection :line_items do
    property :product_id, validates: {presence: true}
    property :quantity,   validates: {numericality: {greater_than: 0}}
  end
end

I'm still not there so I haven't yet thought about how am I going to test those nested properties validations from the collection, maybe I should extract them into another form and just include them there, or maybe that'll be a nice excuse for the reform-matchers creation :smile:

zavan avatar Dec 05 '14 00:12 zavan

LOL yes it is.

musaffa avatar Dec 05 '14 01:12 musaffa

@zavan is this way of using shoulda matchers is still working with reform 2.0 and latest trailblazer?

I'm getting a wrong number of arguments (0 for 1) on reform-1.2.6/lib/reform/contract/setup.rb:4

ps: I'm using Reform::Form.reform_2_0! on initializer

thank you

fernandes avatar Jun 14 '15 19:06 fernandes

as I've checked:

shoulda uniqueness matcher instantiates a new model using Model.new and setup needs the model as parameter

ps: I tested with presence matcher and worked fine

FYI, for uniqueness I've made my own test:

context "with existing domain"do
  it "error existing domain" do
    params = attributes_for(:dummy)

    Dummy::Create[params].model
    dummy = Dummy::Create.run(params).last
    expect(dummy.contract.errors[:domain]).to include('has already been taken')
  end
end

if someone has a better idea on make this, I'm all ears :wink: thank you!

fernandes avatar Jun 14 '15 20:06 fernandes

I do uniqueness tests like that, too.

We should provide our own matchers in addition to what exists! Is there any way we could use the matchers from above plus our own uniqueness, etc? In Rspec AND MiniTest (I don't use Rspec)?

apotonick avatar Jun 14 '15 22:06 apotonick

The more tests I write with forms the more redundancy I can see, so I'm really all in for Reform/Operation matchers. Not that redundancy in tests is wrong, but simple matchers will save time and brain power when writing tests.

We have the advantage that we have data schema and validations cleanly separated from models in the operation, so it should be a nice task to implement matchers.

apotonick avatar Jun 14 '15 22:06 apotonick

the format

describe "the contract" do
    subject { Product::Create.present(params).contract }

    it { is_expected.to validate_presence_of :name }
end

not work anymore since 2.0.3 because valid? become private

shingara avatar Oct 14 '15 11:10 shingara

@shingara Yup, not working anymore. valid? is private and breaks stuff.

Failure/Error: it{ should validate_presence_of :text }
     NoMethodError:
       private method `valid?' called for #<Barbell::News::Create:0x007f9b6ad40668>

karlingen avatar Oct 27 '15 15:10 karlingen

I managed to workaround this by adding the following to my form:

def valid?
  super
end

Is there a good reason why this method is private? @apotonick

karlingen avatar Oct 27 '15 15:10 karlingen

Yepp! :grin: The form's API is result = validate(params) and that's the only single entry point to invoke that logic. I want to reduce state in the form and flatten the public API, which is why we won't have valid? at all in future versions.

Just because ActiveRecord allows this doesn't mean it's the right thing to do. The opposite is the case: people have to understand that there's only one API method to run a validation and retrieve the result.

Using isolated matchers is wrong, in my opinion. A form is a collection of validations that might have side-effects, testing them step-wise in isolation contradicts testing them at all, because you're not testing a production scenario (it will never happen that you have a form submission with one field being submitted, only).

I agree that there should be testing helpers to quickly test formal standard validations, but matchers the way you use it are not the right way. The form has to be fully submitted via validate. Again, this is something Rails implemented and now everyone thinks this is the way to go.

Imagine you had custom validations that rely on each other. Side-effects in your artificial must_validate_presence matcher will never catch those. Can you show me some examples of how you test forms presently?

In Trailblazer, and this is where all my gems are heading, you don't test forms as a stand-alone entity anymore but the entire operation, which assures all side-effects are covered.

Sorry if that doesn't help you but the same discussion is somewhere in these issues from around 6 months ago... :laughing:

apotonick avatar Oct 27 '15 22:10 apotonick

Thanks for the clarification!

I'm writing a gem called deadlift that uses Contracts from your gem and calls them Barbells so they can be used by the deadlift objects.

This is how I test a barbell (contract):

# app/business_logic/deadlifts/news/barbells/create.rb
module Deadlifts
  module News
    module Barbells
      class Create < Deadlift::Barbell::Base
        model :news

        property :title
        property :text

        property :location
        property :current_user

        validates :title, presence: true
        validates :text, presence: true
      end
    end
  end
end
# spec/business_logic/deadlifts/news/barbells/create_spec.rb
require 'spec_helper'

describe Deadlifts::News::Barbells::Create, type: :model do
  let(:params){{}}
  subject{described_class.new(params)}

  it{ should have_model_name :news }

  it{ should have_property :location }
  it{ should have_property :current_user }

  it{ should have_property :text }
  it{ should have_property :title }

  it{ should validate_presence_of :title }
  it{ should validate_presence_of :text }
end

And these are my custom matchers:

# spec/support/matchers/have_property.rb
RSpec::Matchers.define :have_property do |expected|
  match do |actual|
    if actual.respond_to?(expected) && actual.respond_to?("#{expected}=")
      true
    else
      false
    end
  end

  failure_message_when_negated do |actual|
    "expected that #{actual} did not have a property named #{expected}"
  end

  failure_message do |actual|
    "expected that #{actual} had a property named #{expected}"
  end

  description do
    "have the property #{expected}"
  end
end

RSpec::Matchers.define :have_model_name do |expected|
  match do |actual|
    actual.class.model_name.to_s == expected.to_s.camelize
  end

  failure_message do |actual|
    "expected that #{actual} had a model name #{expected}"
  end

  failure_message_when_negated do |actual|
    "expected that #{actual} did not model name #{expected}"
  end

  description do
    "have the model name #{expected}"
  end
end

karlingen avatar Oct 28 '15 07:10 karlingen

Nice stuff! I just checkout out your gem, and it's very similar to Trailblazer::Operation! :grimacing:

apotonick avatar Oct 28 '15 10:10 apotonick

Wow, I wish that I had found out about Trailblazer earlier! Removing my gem now.. 😂

karlingen avatar Oct 28 '15 18:10 karlingen

I just chatted with @apotonick, and he's very open to creating a new gem for the Reform matchers.

valscion avatar May 09 '16 11:05 valscion