clowne-rb/clowne

View on GitHub
docs/testing.md

Summary

Maintainability
Test Coverage
# Testing

Clowne provides specific tools to help you test your cloners.

The main goal is to make it possible to test different cloning phases separately and avoid _heavy_ tests setup phases.

Let's consider the following models and cloners:

```ruby
# app/models/user.rb
class User < ApplicationRecord
  has_one :profile
  has_many :posts
end

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments
  has_many :votes

  scope :draft, -> { where(draft: true) }
end

# app/cloners/user_cloner.rb
class UserCloner < Clowne::Cloner
  class ProfileCloner
    nullify :rating
  end

  include_association :profile, clone_with: ProfileCloner

  nullify :email

  finalize do |_, record, name: nil, **|
    record.name = name unless name.nil?
  end

  trait :copy do
    init_as do |user, target:, **|
      # copy name
      target.name = user.name
      target
    end
  end

  trait :with_posts do
    include_association :posts, :draft, traits: :mark_as_copy
  end

  trait :with_popular_posts do
    include_association :posts, (lambda do |params|
      where("rating > ?", params[:min_rating])
    end)
  end
end

# app/cloners/post_cloner.rb
class PostCloner < Clowne::Cloner
  include_association :comments

  trait :mark_as_copy do |_, record|
    record.title += " (copy)"
  end
end
```

## Getting started

Currently, only [RSpec](http://rspec.info/) is supported.

Add this line to your `spec_helper.rb` (or `rails_helper.rb`):

```ruby
require "clowne/rspec"
```

## Configuration matchers

There are several matchers that allow you to verify the cloner configuration.

### `clone_associations`

This matcher vefifies that your cloner includes the specified associations:

```ruby
# spec/cloners/user_cloner_spec.rb
RSpec.describe UserCloner, type: :cloner do
  subject { described_class }

  specify do
    # checks that only the specified associations is included
    is_expected.to clone_associations(:profile)

    # with traits
    is_expected.to clone_associations(:profile, :posts)
      .with_traits(:with_posts)

    # raises when there are some unspecified associations
    is_expected.to clone_associations(:profile)
      .with_traits(:with_posts)
    #=> raises RSpec::Expectations::ExpectationNotMetError
  end
end
```

### `clone_association`

This matcher allows to verify the specified association options:

```ruby
# spec/cloners/user_cloner_spec.rb
RSpec.describe UserCloner, type: :cloner do
  subject { described_class }

  specify do
    # simply check that association is included
    is_expected.to clone_association(:profile)

    # check options
    is_expected.to clone_association(
      :profile,
      clone_with: described_class::ProfileCloner
    )

    # with traits, scope and activated trait
    is_expected.to clone_association(
      :posts,
      traits: :mark_as_copy,
      scope: :draft
    ).with_traits(:with_posts)
  end
end
```

**NOTE:** `clone_associations`/`clone_association` matchers are only available in groups marked with `type: :cloner` tag.

Clowne automaticaly marks all specs in `spec/cloners` folder with `type: :cloner`. Otherwise you have to add this tag you.


## Using partial cloning

Under the hood, Clowne builds a [compilation plan](architecture.md) which is used to clone the record.

Plan is a set of _actions_ (such as `nullify`, `finalize`, `association`, `init_as`) which are applied to the record.

Most of the time these actions don't depend on each other, thus we can test them separately:

```ruby
# spec/cloners/user_cloner_spec.rb
RSpec.describe UserCloner, type: :cloner do
  subject(:user) { create :user, name: "Bombon" }

  specify "simple case" do
    # apply only the specified part of the plan
    cloned_user = described_class.partial_apply(:nullify, user).to_record
    expect(cloned_user.email).to be_nil
    # finalize wasn't applied
    expect(cloned_user.name).to eq "Bombon"
  end

  specify "with params" do
    cloned_user = described_class.partial_apply(:finalize, user, name: "new name").to_record
    # nullify actions were not applied!
    expect(cloned_user.email).to eq user.email
    # finalize was applied
    expect(cloned_user.name).to eq "new name"
  end

  specify "with traits" do
    a_user = create(:user, name: "Dindon")
    cloned_user = described_class.partial_apply(
      :init_as, user, traits: :copy, target: a_user
    ).to_record
    # returned user is the same as target
    expect(cloned_user).to be_eql(a_user)
    expect(cloned_user.name).to eq "Bombon"
  end

  specify "associations" do
    create(:post, user: user, rating: 1, text: "Boom Boom")
    create(:post, user: user, rating: 2, text: "Flying Dumplings")

    # you can specify which associations to include (you can use array)
    # to apply all associations write:
    #   plan.apply(:association)
    cloned_user = described_class.partial_apply(
      "association.posts", user, traits: :with_popular_posts, min_rating: 1
    ).to_record

    expect(cloned_user.posts.size).to eq 1
    expect(cloned_user.posts.first.text).to eq "Flying Dumplings"
  end
end
```