Commit e7741b0e authored by Lin Jen-Shin's avatar Lin Jen-Shin Committed by Douglas Barbosa Alexandre
Browse files

CE: Absorb product into factory

parent 8bc86614
...@@ -39,7 +39,6 @@ module Key ...@@ -39,7 +39,6 @@ module Key
module Factory module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator' autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base' autoload :Base, 'qa/factory/base'
autoload :Product, 'qa/factory/product'
module Resource module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox' autoload :Sandbox, 'qa/factory/resource/sandbox'
......
...@@ -88,44 +88,6 @@ end ...@@ -88,44 +88,6 @@ end
The [`Project` factory](./resource/project.rb) is a good real example of Browser The [`Project` factory](./resource/project.rb) is a good real example of Browser
UI and API implementations. UI and API implementations.
### Define attributes
After the resource is fabricated, we would like to access the attributes on
the resource. We define the attributes with `attribute` method. Suppose
we want to access the name on the resource, we could change `attr_accessor`
to `attribute`:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attribute :name
# ... same as before
end
end
end
end
```
The difference between `attr_accessor` and `attribute` is that by using
`attribute` it can also be accessed from the product:
```ruby
shirt =
QA::Factory::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.name # => "GitLab QA"
```
In the above example, if we use `attr_accessor :name` then `shirt.name` won't
be available. On the other hand, using `attribute :name` will allow you to use
`shirt.name`, so most of the time you'll want to use `attribute` instead of
`attr_accessor` unless we clearly don't need it for the product.
#### Resource attributes #### Resource attributes
A resource may need another resource to exist first. For instance, a project A resource may need another resource to exist first. For instance, a project
...@@ -145,7 +107,7 @@ module QA ...@@ -145,7 +107,7 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attribute :name attr_accessor :name
attribute :project do attribute :project do
Factory::Resource::Project.fabricate! do |resource| Factory::Resource::Project.fabricate! do |resource|
...@@ -206,7 +168,7 @@ module QA ...@@ -206,7 +168,7 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attribute :name attr_accessor :name
attribute :project do attribute :project do
Factory::Resource::Project.fabricate! do |resource| Factory::Resource::Project.fabricate! do |resource|
...@@ -287,7 +249,7 @@ module QA ...@@ -287,7 +249,7 @@ module QA
shirt_new.create_shirt! shirt_new.create_shirt!
end end
brand # Eagerly construct the data populate(:brand) # Eagerly construct the data
end end
end end
end end
...@@ -295,9 +257,12 @@ module QA ...@@ -295,9 +257,12 @@ module QA
end end
``` ```
This will make sure we construct the data right after we created the shirt. The `populate` method will iterate through its arguments and call each
The drawback for this will become we're forced to construct the data even attribute respectively. Here `populate(:brand)` has the same effect as
if we don't really need to use it. just `brand`. Using the populate method makes the intention clearer.
With this, it will make sure we construct the data right after we create the
shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data.
Alternatively, we could just make sure we're on the right page before Alternatively, we could just make sure we're on the right page before
constructing the brand data: constructing the brand data:
...@@ -307,7 +272,7 @@ module QA ...@@ -307,7 +272,7 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attribute :name attr_accessor :name
attribute :project do attribute :project do
Factory::Resource::Project.fabricate! do |resource| Factory::Resource::Project.fabricate! do |resource|
...@@ -385,7 +350,7 @@ end ...@@ -385,7 +350,7 @@ end
**Notes on attributes precedence:** **Notes on attributes precedence:**
- attributes from the factory have the highest precedence - factory instance variables have the highest precedence
- attributes from the API response take precedence over attributes from the - attributes from the API response take precedence over attributes from the
block (usually from Browser UI) block (usually from Browser UI)
- attributes without a value will raise a `QA::Factory::Base::NoValueError` error - attributes without a value will raise a `QA::Factory::Base::NoValueError` error
......
...@@ -22,12 +22,16 @@ def visit! ...@@ -22,12 +22,16 @@ def visit!
visit(web_url) visit(web_url)
end end
def populate(*attributes)
attributes.each(&method(:public_send))
end
private private
def populate_attribute(name, block) def populate_attribute(name, block)
value = attribute_value(name, block) value = attribute_value(name, block)
raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value
value value
end end
...@@ -84,7 +88,7 @@ def self.do_fabricate!(factory:, prepare_block:, parents: []) ...@@ -84,7 +88,7 @@ def self.do_fabricate!(factory:, prepare_block:, parents: [])
resource_web_url = yield resource_web_url = yield
factory.web_url = resource_web_url factory.web_url = resource_web_url
Factory::Product.new(factory) factory
end end
private_class_method :do_fabricate! private_class_method :do_fabricate!
......
require 'capybara/dsl'
module QA
module Factory
class Product
include Capybara::DSL
attr_reader :factory
def initialize(factory)
@factory = factory
define_attributes
end
def visit!
visit(web_url)
end
def populate(*attributes)
attributes.each(&method(:public_send))
end
private
def define_attributes
factory.class.attributes_names.each do |name|
define_singleton_method(name) do
factory.public_send(name)
end
end
end
end
end
end
...@@ -9,8 +9,6 @@ class ProjectPush < Factory::Repository::Push ...@@ -9,8 +9,6 @@ class ProjectPush < Factory::Repository::Push
end end
end end
attribute :output
def initialize def initialize
@file_name = 'file.txt' @file_name = 'file.txt'
@file_content = '# This is test project' @file_content = '# This is test project'
......
...@@ -50,8 +50,7 @@ def visit_project_with_retry ...@@ -50,8 +50,7 @@ def visit_project_with_retry
end end
def fabricate! def fabricate!
push populate(:push, :user)
user
visit_project_with_retry visit_project_with_retry
......
...@@ -12,8 +12,6 @@ class MergeRequest < Factory::Base ...@@ -12,8 +12,6 @@ class MergeRequest < Factory::Base
:milestone, :milestone,
:labels :labels
attribute :source_branch
attribute :project do attribute :project do
Factory::Resource::Project.fabricate! do |resource| Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-merge-request' resource.name = 'project-with-merge-request'
...@@ -52,8 +50,8 @@ def initialize ...@@ -52,8 +50,8 @@ def initialize
end end
def fabricate! def fabricate!
target populate(:target, :source)
source
project.visit! project.visit!
Page::Project::Show.perform(&:new_merge_request) Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform do |page| Page::MergeRequest::New.perform do |page|
......
...@@ -18,8 +18,10 @@ class MergeRequestFromFork < MergeRequest ...@@ -18,8 +18,10 @@ class MergeRequestFromFork < MergeRequest
end end
def fabricate! def fabricate!
push populate(:push)
fork.visit! fork.visit!
Page::Project::Show.perform(&:new_merge_request) Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform(&:create_merge_request) Page::MergeRequest::New.perform(&:create_merge_request)
end end
......
...@@ -4,14 +4,13 @@ module QA ...@@ -4,14 +4,13 @@ module QA
module Factory module Factory
module Resource module Resource
class ProjectImportedFromGithub < Resource::Project class ProjectImportedFromGithub < Resource::Project
attr_accessor :name
attr_writer :personal_access_token, :github_repository_path attr_writer :personal_access_token, :github_repository_path
attribute :group do attribute :group do
Factory::Resource::Group.fabricate! Factory::Resource::Group.fabricate!
end end
attribute :name
def fabricate! def fabricate!
group.visit! group.visit!
......
...@@ -2,14 +2,13 @@ module QA ...@@ -2,14 +2,13 @@ module QA
module Factory module Factory
module Resource module Resource
class ProjectMilestone < Factory::Base class ProjectMilestone < Factory::Base
attr_reader :title
attr_accessor :description attr_accessor :description
attribute :project do attribute :project do
Factory::Resource::Project.fabricate! Factory::Resource::Project.fabricate!
end end
attribute :title
def title=(title) def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}" @title = "#{title}-#{SecureRandom.hex(4)}"
@description = 'A milestone' @description = 'A milestone'
......
...@@ -9,7 +9,6 @@ class Sandbox < Factory::Base ...@@ -9,7 +9,6 @@ class Sandbox < Factory::Base
attr_reader :path attr_reader :path
attribute :id attribute :id
attribute :path
def initialize def initialize
@path = Runtime::Namespace.sandbox_name @path = Runtime::Namespace.sandbox_name
......
...@@ -6,11 +6,9 @@ module Resource ...@@ -6,11 +6,9 @@ module Resource
class SSHKey < Factory::Base class SSHKey < Factory::Base
extend Forwardable extend Forwardable
def_delegators :key, :private_key, :public_key, :fingerprint attr_accessor :title
attribute :private_key def_delegators :key, :private_key, :public_key, :fingerprint
attribute :title
attribute :fingerprint
def key def key
@key ||= Runtime::Key::RSA.new @key ||= Runtime::Key::RSA.new
......
...@@ -5,6 +5,7 @@ module Factory ...@@ -5,6 +5,7 @@ module Factory
module Resource module Resource
class User < Factory::Base class User < Factory::Base
attr_reader :unique_id attr_reader :unique_id
attr_writer :username, :password
def initialize def initialize
@unique_id = SecureRandom.hex(8) @unique_id = SecureRandom.hex(8)
...@@ -30,11 +31,6 @@ def credentials_given? ...@@ -30,11 +31,6 @@ def credentials_given?
defined?(@username) && defined?(@password) defined?(@username) && defined?(@password)
end end
attribute :name
attribute :username
attribute :email
attribute :password
def fabricate! def fabricate!
# Don't try to log-out if we're not logged-in # Don't try to log-out if we're not logged-in
if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
......
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
include Support::StubENV include Support::StubENV
let(:factory) { spy('factory') } let(:factory) { spy('factory') }
let(:product) { spy('product') } let(:location) { 'http://location' }
let(:product_location) { 'http://product_location' }
shared_context 'fabrication context' do shared_context 'fabrication context' do
subject do subject do
...@@ -17,9 +16,8 @@ def self.name ...@@ -17,9 +16,8 @@ def self.name
end end
before do before do
allow(subject).to receive(:current_url).and_return(product_location) allow(subject).to receive(:current_url).and_return(location)
allow(subject).to receive(:new).and_return(factory) allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product)
end end
end end
...@@ -28,7 +26,7 @@ def self.name ...@@ -28,7 +26,7 @@ def self.name
it 'yields factory before calling factory method' do it 'yields factory before calling factory method' do
expect(factory).to receive(:something!).ordered expect(factory).to receive(:something!).ordered
expect(factory).to receive(fabrication_method_used).ordered.and_return(product_location) expect(factory).to receive(fabrication_method_used).ordered.and_return(location)
subject.public_send(fabrication_method_called, factory: factory) do |factory| subject.public_send(fabrication_method_called, factory: factory) do |factory|
factory.something! factory.something!
...@@ -37,7 +35,7 @@ def self.name ...@@ -37,7 +35,7 @@ def self.name
it 'does not log the factory and build method when QA_DEBUG=false' do it 'does not log the factory and build method when QA_DEBUG=false' do
stub_env('QA_DEBUG', 'false') stub_env('QA_DEBUG', 'false')
expect(factory).to receive(fabrication_method_used).and_return(product_location) expect(factory).to receive(fabrication_method_used).and_return(location)
expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } expect { subject.public_send(fabrication_method_called, 'something', factory: factory) }
.not_to output.to_stdout .not_to output.to_stdout
...@@ -71,17 +69,17 @@ def self.name ...@@ -71,17 +69,17 @@ def self.name
it_behaves_like 'fabrication method', :fabricate_via_api! it_behaves_like 'fabrication method', :fabricate_via_api!
it 'instantiates the factory, calls factory method returns fabrication product' do it 'instantiates the factory, calls factory method returns the resource' do
expect(factory).to receive(:fabricate_via_api!).and_return(product_location) expect(factory).to receive(:fabricate_via_api!).and_return(location)
result = subject.fabricate_via_api!(factory: factory, parents: []) result = subject.fabricate_via_api!(factory: factory, parents: [])
expect(result).to eq(product) expect(result).to eq(factory)
end end
it 'logs the factory and build method when QA_DEBUG=true' do it 'logs the factory and build method when QA_DEBUG=true' do
stub_env('QA_DEBUG', 'true') stub_env('QA_DEBUG', 'true')
expect(factory).to receive(:fabricate_via_api!).and_return(product_location) expect(factory).to receive(:fabricate_via_api!).and_return(location)
expect { subject.fabricate_via_api!(factory: factory, parents: []) } expect { subject.fabricate_via_api!(factory: factory, parents: []) }
.to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/) .to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/)
...@@ -100,10 +98,10 @@ def self.name ...@@ -100,10 +98,10 @@ def self.name
expect(factory).to have_received(:fabricate!).with('something') expect(factory).to have_received(:fabricate!).with('something')
end end
it 'returns fabrication product' do it 'returns fabrication resource' do
result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
expect(result).to eq(product) expect(result).to eq(factory)
end end
it 'logs the factory and build method when QA_DEBUG=true' do it 'logs the factory and build method when QA_DEBUG=true' do
...@@ -140,44 +138,44 @@ def self.current_url ...@@ -140,44 +138,44 @@ def self.current_url
describe '.attribute' do describe '.attribute' do
include_context 'simple factory' include_context 'simple factory'
it 'appends new product attribute' do it 'appends new attribute' do
expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
end end
context 'when the product attribute is populated via a block' do context 'when the attribute is populated via a block' do
it 'returns a fabrication product and defines factory attributes as its methods' do it 'returns value from the block' do
result = subject.fabricate!(factory: factory) result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product) expect(result).to be_a(described_class)
expect(result.test).to eq('block') expect(result.test).to eq('block')
end end
end end
context 'when the product attribute is populated via the api' do context 'when the attribute is populated via the api' do
let(:api_resource) { { no_block: 'api' } } let(:api_resource) { { no_block: 'api' } }
before do before do
expect(factory).to receive(:api_resource).and_return(api_resource) expect(factory).to receive(:api_resource).and_return(api_resource)
end end
it 'returns a fabrication product and defines factory attributes as its methods' do it 'returns value from api' do
result = subject.fabricate!(factory: factory) result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product) expect(result).to be_a(described_class)
expect(result.no_block).to eq('api') expect(result.no_block).to eq('api')
end end
context 'when the attribute also has a block in the factory' do context 'when the attribute also has a block' do
let(:api_resource) { { test: 'api_with_block' } } let(:api_resource) { { test: 'api_with_block' } }
before do before do
allow(QA::Runtime::Logger).to receive(:info) allow(QA::Runtime::Logger).to receive(:info)
end end
it 'returns the api value and emits an INFO log entry' do it 'returns value from api and emits an INFO log entry' do
result = subject.fabricate!(factory: factory) result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product) expect(result).to be_a(described_class)
expect(result.test).to eq('api_with_block') expect(result.test).to eq('api_with_block')
expect(QA::Runtime::Logger) expect(QA::Runtime::Logger)
.to have_received(:info).with(/api_with_block/) .to have_received(:info).with(/api_with_block/)
...@@ -185,15 +183,15 @@ def self.current_url ...@@ -185,15 +183,15 @@ def self.current_url
end end
end end
context 'when the product attribute is populated via a factory attribute' do context 'when the attribute is populated via direct assignment' do
before do before do
factory.test = 'value' factory.test = 'value'
end end