Always use `attribute` to define the product

This commit is contained in:
Lin Jen-Shin 2018-10-25 20:29:24 +00:00 committed by Douglas Barbosa Alexandre
parent bf96ec85c7
commit 5151801964
34 changed files with 557 additions and 637 deletions

View File

@ -39,7 +39,6 @@ module QA
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 :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product' autoload :Product, 'qa/factory/product'
module Resource module Resource

View File

@ -26,11 +26,7 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attr_accessor :name, :size attr_accessor :name
def initialize(name)
@name = name
end
def fabricate! def fabricate!
Page::Dashboard::Index.perform do |dashboard_index| Page::Dashboard::Index.perform do |dashboard_index|
@ -64,21 +60,10 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attr_accessor :name, :size attr_accessor :name
def initialize(name)
@name = name
end
def fabricate! def fabricate!
Page::Dashboard::Index.perform do |dashboard_index| # ... same as before
dashboard_index.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end end
def api_get_path def api_get_path
@ -103,33 +88,69 @@ 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 dependencies ### Define attributes
A resource may need an other resource to exist first. For instance, a project After the resource is fabricated, we would like to access the attributes on
needs a group to be created in. 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 define a dependency, you can use the `dependency` DSL method. to `attribute`:
The first argument is a factory class, then you should pass `as: <name>` to give
a name to the dependency.
That will allow access to the dependency from your resource object's methods.
You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`,
`#api_post_body`.
Let's take the `Shirt` factory, and add a `project` dependency to it:
```ruby ```ruby
module QA module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attr_accessor :name, :size attribute :name
dependency Factory::Resource::Project, as: :project do |project| # ... same as before
project.name = 'project-to-create-a-shirt' end
end end
end
end
```
def initialize(name) The difference between `attr_accessor` and `attribute` is that by using
@name = name `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
A resource may need another resource to exist first. For instance, a project
needs a group to be created in.
To define a resource attribute, you can use the `attribute` method with a
block using the other factory to fabricate the resource.
That will allow access to the other resource from your resource object's
methods. You would usually use it in `#fabricate!`, `#api_get_path`,
`#api_post_path`, `#api_post_body`.
Let's take the `Shirt` factory, and add a `project` attribute to it:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attribute :name
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-create-a-shirt'
end
end end
def fabricate! def fabricate!
@ -164,19 +185,19 @@ module QA
end end
``` ```
**Note that dependencies are always built via the API fabrication method if **Note that all the attributes are lazily constructed. This means if you want
supported by their factories.** a specific attribute to be fabricated first, you'll need to call the
attribute method first even if you're not using it.**
### Define attributes on the created resource #### Product data attributes
Once created, you may want to populate a resource with attributes that can be Once created, you may want to populate a resource with attributes that can be
found in the Web page, or in the API response. found in the Web page, or in the API response.
For instance, once you create a project, you may want to store its repository For instance, once you create a project, you may want to store its repository
SSH URL as an attribute. SSH URL as an attribute.
To define an attribute, you can use the `product` DSL method. Again we could use the `attribute` method with a block, using a page object
The first argument is the attribute name, then you should define a name for the to retrieve the data on the page.
dependency to be accessible from your resource object's methods.
Let's take the `Shirt` factory, and define a `:brand` attribute: Let's take the `Shirt` factory, and define a `:brand` attribute:
@ -185,90 +206,74 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attr_accessor :name, :size attribute :name
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-to-create-a-shirt' Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-create-a-shirt'
end
end end
# Attribute populated from the Browser UI (using the block) # Attribute populated from the Browser UI (using the block)
product :brand do attribute :brand do
Page::Shirt::Show.perform do |shirt_show| Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page shirt_show.fetch_brand_from_page
end end
end end
def initialize(name) # ... same as before
@name = name
end
def fabricate!
project.visit!
Page::Project::Show.perform do |project_show|
project_show.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
end end
end end
end end
end end
``` ```
#### Inherit a factory's attribute **Note again that all the attributes are lazily constructed. This means if
you call `shirt.brand` after moving to the other page, it'll not properly
retrieve the data because we're no longer on the expected page.**
Sometimes, you want a resource to inherit its factory attributes. For instance, Consider this:
it could be useful to pass the `size` attribute from the `Shirt` factory to the
created resource.
You can do that by defining `product :attribute_name` without a block.
Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes: ```ruby
shirt =
QA::Factory::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.project.visit!
shirt.brand # => FAIL!
```
The above example will fail because now we're on the project page, trying to
construct the brand data from the shirt page, however we moved to the project
page already. There are two ways to solve this, one is that we could try to
retrieve the brand before visiting the project again:
```ruby
shirt =
QA::Factory::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.brand # => OK!
shirt.project.visit!
shirt.brand # => OK!
```
The attribute will be stored in the instance therefore all the following calls
will be fine, using the data previously constructed. If we think that this
might be too brittle, we could eagerly construct the data right before
ending fabrication:
```ruby ```ruby
module QA module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attr_accessor :name, :size # ... same as before
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
# Attribute from the Browser UI (using the block)
product :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
# Attribute inherited from the Shirt factory if present,
# or a QA::Factory::Product::NoValueError is raised otherwise
product :name
product :size
def initialize(name)
@name = name
end
def fabricate! def fabricate!
project.visit! project.visit!
@ -281,20 +286,8 @@ module QA
shirt_new.set_name(name) shirt_new.set_name(name)
shirt_new.create_shirt! shirt_new.create_shirt!
end end
end
def api_get_path brand # Eagerly construct the data
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end end
end end
end end
@ -302,6 +295,48 @@ module QA
end end
``` ```
This will make sure we construct the data right after we created the shirt.
The drawback for this will become we're forced to construct the data even
if we don't really need to use it.
Alternatively, we could just make sure we're on the right page before
constructing the brand data:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attribute :name
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-create-a-shirt'
end
end
# Attribute populated from the Browser UI (using the block)
attribute :brand do
back_url = current_url
visit!
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
visit(back_url)
end
# ... same as before
end
end
end
end
```
This will make sure it's on the shirt page before constructing brand, and
move back to the previous page to avoid breaking the state.
#### Define an attribute based on an API response #### Define an attribute based on an API response
Sometimes, you want to define a resource attribute based on the API response Sometimes, you want to define a resource attribute based on the API response
@ -311,7 +346,6 @@ the API returns
```ruby ```ruby
{ {
brand: 'a-brand-new-brand', brand: 'a-brand-new-brand',
size: 'extra-small',
style: 't-shirt', style: 't-shirt',
materials: [[:cotton, 80], [:polyamide, 20]] materials: [[:cotton, 80], [:polyamide, 20]]
} }
@ -320,18 +354,6 @@ the API returns
you may want to store `style` as-is in the resource, and fetch the first value you may want to store `style` as-is in the resource, and fetch the first value
of the first `materials` item in a `main_fabric` attribute. of the first `materials` item in a `main_fabric` attribute.
For both attributes, you will need to define an inherited attribute, as shown
in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you
will need to implement the
`#transform_api_resource` method to first populate the `:main_fabric` key in the
API response so that it can be used later to automatically populate the
attribute on your resource.
If an attribute can only be retrieved from the API response, you should define
a block to give it a default value, otherwise you could get a
`QA::Factory::Product::NoValueError` when creating your resource via the
Browser UI.
Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
attributes: attributes:
@ -340,69 +362,21 @@ module QA
module Factory module Factory
module Resource module Resource
class Shirt < Factory::Base class Shirt < Factory::Base
attr_accessor :name, :size # ... same as before
dependency Factory::Resource::Project, as: :project do |project| # Attribute from the Shirt factory if present,
project.name = 'project-to-create-a-shirt' # or fetched from the API response if present,
# or a QA::Factory::Base::NoValueError is raised otherwise
attribute :style
# If the attribute from the Shirt factory is not present,
# and if the API does not contain this field, this block will be
# used to construct the value based on the API response.
attribute :main_fabric do
api_response.&dig(:materials, 0, 0)
end end
# Attribute fetched from the API response if present, # ... same as before
# or from the Browser UI otherwise (using the block)
product :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
# Attribute fetched from the API response if present,
# or from the Shirt factory if present,
# or a QA::Factory::Product::NoValueError is raised otherwise
product :name
product :size
product :style do
'unknown'
end
product :main_fabric do
'unknown'
end
def initialize(name)
@name = name
end
def fabricate!
project.visit!
Page::Project::Show.perform do |project_show|
project_show.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
private
def transform_api_resource(api_response)
api_response[:main_fabric] = api_response[:materials][0][0]
api_response
end
end end
end end
end end
@ -411,11 +385,10 @@ end
**Notes on attributes precedence:** **Notes on attributes precedence:**
- attributes from the factory 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
Browser UI block (usually from Browser UI)
- attributes from the Browser UI take precedence over attributes from the - attributes without a value will raise a `QA::Factory::Base::NoValueError` error
factory (i.e inherited)
- attributes without a value will raise a `QA::Factory::Product::NoValueError` error
## Creating resources in your tests ## Creating resources in your tests
@ -428,42 +401,40 @@ Here is an example that will use the API fabrication method under the hood since
it's supported by the `Shirt` factory: it's supported by the `Shirt` factory:
```ruby ```ruby
my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt| my_shirt = Factory::Resource::Shirt.fabricate! do |shirt|
shirt.size = 'small' shirt.name = 'my-shirt'
end end
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response
expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
``` ```
If you explicitely want to use the Browser UI fabrication method, you can call If you explicitly want to use the Browser UI fabrication method, you can call
the `.fabricate_via_browser_ui!` method instead: the `.fabricate_via_browser_ui!` method instead:
```ruby ```ruby
my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt| my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt|
shirt.size = 'small' shirt.name = 'my-shirt'
end end
expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided
expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block
``` ```
You can also explicitely use the API fabrication method, by calling the You can also explicitly use the API fabrication method, by calling the
`.fabricate_via_api!` method: `.fabricate_via_api!` method:
```ruby ```ruby
my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt| my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
shirt.size = 'small' shirt.name = 'my-shirt'
end end
``` ```
In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`. In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
## Where to ask for help? ## Where to ask for help?

View File

@ -10,13 +10,42 @@ module QA
include ApiFabricator include ApiFabricator
extend Capybara::DSL extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies NoValueError = Class.new(RuntimeError)
def_delegators :evaluator, :product, :attributes
def_delegators :evaluator, :attribute
def fabricate!(*_args) def fabricate!(*_args)
raise NotImplementedError raise NotImplementedError
end end
def visit!
visit(web_url)
end
private
def populate_attribute(name, block)
value = attribute_value(name, block)
raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value
value
end
def attribute_value(name, block)
api_value = api_resource&.dig(name)
if api_value && block
log_having_both_api_result_and_block(name, api_value)
end
api_value || (block && instance_exec(&block))
end
def log_having_both_api_result_and_block(name, api_value)
QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
end
def self.fabricate!(*args, &prepare_block) def self.fabricate!(*args, &prepare_block)
fabricate_via_api!(*args, &prepare_block) fabricate_via_api!(*args, &prepare_block)
rescue NotImplementedError rescue NotImplementedError
@ -52,13 +81,10 @@ module QA
def self.do_fabricate!(factory:, prepare_block:, parents: []) def self.do_fabricate!(factory:, prepare_block:, parents: [])
prepare_block.call(factory) if prepare_block prepare_block.call(factory) if prepare_block
dependencies.each do |signature|
Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
end
resource_web_url = yield resource_web_url = yield
factory.web_url = resource_web_url
Factory::Product.populate!(factory, resource_web_url) Factory::Product.new(factory)
end end
private_class_method :do_fabricate! private_class_method :do_fabricate!
@ -85,31 +111,40 @@ module QA
end end
private_class_method :evaluator private_class_method :evaluator
class DSL def self.dynamic_attributes
attr_reader :dependencies, :attributes const_get(:DynamicAttributes)
rescue NameError
mod = const_set(:DynamicAttributes, Module.new)
include mod
mod
end
def self.attributes_names
dynamic_attributes.instance_methods(false).sort.grep_v(/=$/)
end
class DSL
def initialize(base) def initialize(base)
@base = base @base = base
@dependencies = []
@attributes = []
end end
def dependency(factory, as:, &block) def attribute(name, &block)
as.tap do |name| @base.dynamic_attributes.module_eval do
@base.class_eval { attr_accessor name } attr_writer(name)
Dependency::Signature.new(name, factory, block).tap do |signature| define_method(name) do
@dependencies << signature instance_variable_get("@#{name}") ||
instance_variable_set(
"@#{name}",
populate_attribute(name, block))
end end
end end
end end
def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature|
@attributes << signature
end
end
end end
attribute :web_url
end end
end end
end end

View File

@ -1,28 +0,0 @@
module QA
module Factory
class Dependency
Signature = Struct.new(:name, :factory, :block)
def initialize(caller_factory, dependency_signature)
@caller_factory = caller_factory
@dependency_signature = dependency_signature
end
def overridden?
!!@caller_factory.public_send(@dependency_signature.name)
end
def build!(parents: [])
return if overridden?
dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@dependency_signature.block&.call(factory, @caller_factory)
end
dependency.tap do |dependency|
@caller_factory.public_send("#{@dependency_signature.name}=", dependency)
end
end
end
end
end

View File

@ -5,46 +5,31 @@ module QA
class Product class Product
include Capybara::DSL include Capybara::DSL
NoValueError = Class.new(RuntimeError) attr_reader :factory
attr_reader :factory, :web_url def initialize(factory)
Attribute = Struct.new(:name, :block)
def initialize(factory, web_url)
@factory = factory @factory = factory
@web_url = web_url
populate_attributes! define_attributes
end end
def visit! def visit!
visit(web_url) visit(web_url)
end end
def self.populate!(factory, web_url) def populate(*attributes)
new(factory, web_url) attributes.each(&method(:public_send))
end end
private private
def populate_attributes! def define_attributes
factory.class.attributes.each do |attribute| factory.class.attributes_names.each do |name|
instance_exec(factory, attribute.block) do |factory, block| define_singleton_method(name) do
value = attribute_value(attribute, block) factory.public_send(name)
raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value
define_singleton_method(attribute.name) { value }
end end
end end
end end
def attribute_value(attribute, block)
factory.api_resource&.dig(attribute.name) ||
(block && block.call(factory)) ||
(factory.respond_to?(attribute.name) && factory.public_send(attribute.name))
end
end end
end end
end end

View File

@ -2,13 +2,14 @@ module QA
module Factory module Factory
module Repository module Repository
class ProjectPush < Factory::Repository::Push class ProjectPush < Factory::Repository::Push
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-with-code' Factory::Resource::Project.fabricate! do |resource|
project.description = 'Project with repository' resource.name = 'project-with-code'
resource.description = 'Project with repository'
end
end end
product :output attribute :output
product :project
def initialize def initialize
@file_name = 'file.txt' @file_name = 'file.txt'

View File

@ -2,10 +2,12 @@ module QA
module Factory module Factory
module Repository module Repository
class WikiPush < Factory::Repository::Push class WikiPush < Factory::Repository::Push
dependency Factory::Resource::Wiki, as: :wiki do |wiki| attribute :wiki do
wiki.title = 'Home' Factory::Resource::Wiki.fabricate! do |resource|
wiki.content = '# My First Wiki Content' resource.title = 'Home'
wiki.message = 'Update home' resource.content = '# My First Wiki Content'
resource.message = 'Update home'
end
end end
def initialize def initialize

View File

@ -5,8 +5,10 @@ module QA
attr_accessor :project, :branch_name, attr_accessor :project, :branch_name,
:allow_to_push, :allow_to_merge, :protected :allow_to_push, :allow_to_merge, :protected
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'protected-branch-project' Factory::Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end end
def initialize def initialize
@ -43,9 +45,7 @@ module QA
# to `allow_to_push` variable. # to `allow_to_push` variable.
return branch unless @protected return branch unless @protected
Page::Project::Menu.act do Page::Project::Menu.perform(&:click_repository_settings)
click_repository_settings
end
Page::Project::Settings::Repository.perform do |setting| Page::Project::Settings::Repository.perform do |setting|
setting.expand_protected_branches do |page| setting.expand_protected_branches do |page|

View File

@ -4,11 +4,11 @@ module QA
class DeployKey < Factory::Base class DeployKey < Factory::Base
attr_accessor :title, :key attr_accessor :title, :key
product :fingerprint do |resource| attribute :fingerprint do
Page::Project::Settings::Repository.act do Page::Project::Settings::Repository.perform do |setting|
expand_deploy_keys do |key| setting.expand_deploy_keys do |key|
key_offset = key.key_titles.index do |title| key_offset = key.key_titles.index do |key_title|
title.text == resource.title key_title.text == title
end end
key.key_fingerprints[key_offset].text key.key_fingerprints[key_offset].text
@ -16,17 +16,17 @@ module QA
end end
end end
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-to-deploy' Factory::Resource::Project.fabricate! do |resource|
project.description = 'project for adding deploy key test' resource.name = 'project-to-deploy'
resource.description = 'project for adding deploy key test'
end
end end
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Menu.act do Page::Project::Menu.perform(&:click_repository_settings)
click_repository_settings
end
Page::Project::Settings::Repository.perform do |setting| Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |page| setting.expand_deploy_keys do |page|

View File

@ -4,25 +4,27 @@ module QA
class DeployToken < Factory::Base class DeployToken < Factory::Base
attr_accessor :name, :expires_at attr_accessor :name, :expires_at
product :username do |resource| attribute :username do
Page::Project::Settings::Repository.act do Page::Project::Settings::Repository.perform do |page|
expand_deploy_tokens do |token| page.expand_deploy_tokens do |token|
token.token_username token.token_username
end end
end end
end end
product :password do |password| attribute :password do
Page::Project::Settings::Repository.act do Page::Project::Settings::Repository.perform do |page|
expand_deploy_tokens do |token| page.expand_deploy_tokens do |token|
token.token_password token.token_password
end end
end end
end end
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-to-deploy' Factory::Resource::Project.fabricate! do |resource|
project.description = 'project for adding deploy token test' resource.name = 'project-to-deploy'
resource.description = 'project for adding deploy token test'
end
end end
def fabricate! def fabricate!

View File

@ -8,8 +8,10 @@ module QA
:content, :content,
:commit_message :commit_message
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-with-new-file' Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-new-file'
end
end end
def initialize def initialize
@ -21,7 +23,7 @@ module QA
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Show.act { create_new_file! } Page::Project::Show.perform(&:create_new_file!)
Page::File::Form.perform do |page| Page::File::Form.perform do |page|
page.add_name(@name) page.add_name(@name)

View File

@ -2,16 +2,18 @@ module QA
module Factory module Factory
module Resource module Resource
class Fork < Factory::Base class Fork < Factory::Base
dependency Factory::Repository::ProjectPush, as: :push attribute :push do
Factory::Repository::ProjectPush.fabricate!
dependency Factory::Resource::User, as: :user do |user|
if Runtime::Env.forker?
user.username = Runtime::Env.forker_username
user.password = Runtime::Env.forker_password
end
end end
product :user attribute :user do
Factory::Resource::User.fabricate! do |resource|
if Runtime::Env.forker?
resource.username = Runtime::Env.forker_username
resource.password = Runtime::Env.forker_password
end
end
end
def visit_project_with_retry def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the # The user intermittently fails to stay signed in after visiting the
@ -48,15 +50,20 @@ module QA
end end
def fabricate! def fabricate!
push
user
visit_project_with_retry visit_project_with_retry
Page::Project::Show.act { fork_project } Page::Project::Show.perform(&:fork_project)
Page::Project::Fork::New.perform do |fork_new| Page::Project::Fork::New.perform do |fork_new|
fork_new.choose_namespace(user.name) fork_new.choose_namespace(user.name)
end end
Page::Layout::Banner.act { has_notice?('The project was successfully forked.') } Page::Layout::Banner.perform do |page|
page.has_notice?('The project was successfully forked.')
end
end end
end end
end end

View File

@ -4,12 +4,12 @@ module QA
class Group < Factory::Base class Group < Factory::Base
attr_accessor :path, :description attr_accessor :path, :description
dependency Factory::Resource::Sandbox, as: :sandbox attribute :sandbox do
Factory::Resource::Sandbox.fabricate!
product :id do
true # We don't retrieve the Group ID when using the Browser UI
end end
attribute :id
def initialize def initialize
@path = Runtime::Namespace.name @path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}" @description = "QA test run at #{Runtime::Namespace.time}"

View File

@ -2,22 +2,21 @@ module QA
module Factory module Factory
module Resource module Resource
class Issue < Factory::Base class Issue < Factory::Base
attr_accessor :title, :description, :project attr_writer :description
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-for-issues' Factory::Resource::Project.fabricate! do |resource|
project.description = 'project for adding issues' resource.name = 'project-for-issues'
resource.description = 'project for adding issues'
end
end end
product :project attribute :title
product :title
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Show.act do Page::Project::Show.perform(&:go_to_new_issue)
go_to_new_issue
end
Page::Project::Issue::New.perform do |page| Page::Project::Issue::New.perform do |page|
page.add_title(@title) page.add_title(@title)

View File

@ -7,24 +7,21 @@ module QA
attr_writer :project, :cluster, attr_writer :project, :cluster,
:install_helm_tiller, :install_ingress, :install_prometheus, :install_runner :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
product :ingress_ip do attribute :ingress_ip do
Page::Project::Operations::Kubernetes::Show.perform do |page| Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
page.ingress_ip
end
end end
def fabricate! def fabricate!
@project.visit! @project.visit!
Page::Project::Menu.act { click_operations_kubernetes } Page::Project::Menu.perform(
&:click_operations_kubernetes)
Page::Project::Operations::Kubernetes::Index.perform do |page| Page::Project::Operations::Kubernetes::Index.perform(
page.add_kubernetes_cluster &:add_kubernetes_cluster)
end
Page::Project::Operations::Kubernetes::Add.perform do |page| Page::Project::Operations::Kubernetes::Add.perform(
page.add_existing_cluster &:add_existing_cluster)
end
Page::Project::Operations::Kubernetes::AddExisting.perform do |page| Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
page.set_cluster_name(@cluster.cluster_name) page.set_cluster_name(@cluster.cluster_name)

View File

@ -4,14 +4,14 @@ module QA
module Factory module Factory
module Resource module Resource
class Label < Factory::Base class Label < Factory::Base
attr_accessor :title, attr_accessor :description, :color
:description,
:color
product(:title) { |factory| factory.title } attribute :title
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-with-label' Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-label'
end
end end
def initialize def initialize
@ -23,8 +23,8 @@ module QA
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Menu.act { go_to_labels } Page::Project::Menu.perform(&:go_to_labels)
Page::Label::Index.act { go_to_new_label } Page::Label::Index.perform(&:go_to_new_label)
Page::Label::New.perform do |page| Page::Label::New.perform do |page|
page.fill_title(@title) page.fill_title(@title)

View File

@ -12,27 +12,33 @@ module QA
:milestone, :milestone,
:labels :labels
product :project attribute :source_branch
product :source_branch
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-with-merge-request' Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-merge-request'
end
end end
dependency Factory::Repository::ProjectPush, as: :target do |push, factory| attribute :target do
factory.project.visit! project.visit!
push.project = factory.project
push.branch_name = 'master' Factory::Repository::ProjectPush.fabricate! do |resource|
push.remote_branch = factory.target_branch resource.project = project
resource.branch_name = 'master'
resource.remote_branch = target_branch
end
end end
dependency Factory::Repository::ProjectPush, as: :source do |push, factory| attribute :source do
push.project = factory.project Factory::Repository::ProjectPush.fabricate! do |resource|
push.branch_name = factory.target_branch resource.project = project
push.remote_branch = factory.source_branch resource.branch_name = target_branch
push.new_branch = false resource.remote_branch = source_branch
push.file_name = "added_file.txt" resource.new_branch = false
push.file_content = "File Added" resource.file_name = "added_file.txt"
resource.file_content = "File Added"
end
end end
def initialize def initialize
@ -46,8 +52,10 @@ module QA
end end
def fabricate! def fabricate!
target
source
project.visit! project.visit!
Page::Project::Show.act { new_merge_request } Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform do |page| Page::MergeRequest::New.perform do |page|
page.fill_title(@title) page.fill_title(@title)
page.fill_description(@description) page.fill_description(@description)

View File

@ -4,19 +4,24 @@ module QA
class MergeRequestFromFork < MergeRequest class MergeRequestFromFork < MergeRequest
attr_accessor :fork_branch attr_accessor :fork_branch
dependency Factory::Resource::Fork, as: :fork attribute :fork do
Factory::Resource::Fork.fabricate!
end
dependency Factory::Repository::ProjectPush, as: :push do |push, factory| attribute :push do
push.project = factory.fork Factory::Repository::ProjectPush.fabricate! do |resource|
push.branch_name = factory.fork_branch resource.project = fork
push.file_name = 'file2.txt' resource.branch_name = fork_branch
push.user = factory.fork.user resource.file_name = 'file2.txt'
resource.user = fork.user
end
end end
def fabricate! def fabricate!
push
fork.visit! fork.visit!
Page::Project::Show.act { new_merge_request } Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.act { create_merge_request } Page::MergeRequest::New.perform(&:create_merge_request)
end end
end end
end end

View File

@ -7,13 +7,13 @@ module QA
class PersonalAccessToken < Factory::Base class PersonalAccessToken < Factory::Base
attr_accessor :name attr_accessor :name
product :access_token do attribute :access_token do
Page::Profile::PersonalAccessTokens.act { created_access_token } Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
end end
def fabricate! def fabricate!
Page::Main::Menu.act { go_to_profile_settings } Page::Main::Menu.perform(&:go_to_profile_settings)
Page::Profile::Menu.act { click_access_tokens } Page::Profile::Menu.perform(&:click_access_tokens)
Page::Profile::PersonalAccessTokens.perform do |page| Page::Profile::PersonalAccessTokens.perform do |page|
page.fill_token_name(name || 'api-test-token') page.fill_token_name(name || 'api-test-token')

View File

@ -4,25 +4,24 @@ module QA
module Factory module Factory
module Resource module Resource
class Project < Factory::Base class Project < Factory::Base
attr_accessor :description attribute :name
attr_reader :name attribute :description
dependency Factory::Resource::Group, as: :group attribute :group do
Factory::Resource::Group.fabricate!
end
product :group attribute :repository_ssh_location do
product :name Page::Project::Show.perform do |page|
page.choose_repository_clone_ssh
product :repository_ssh_location do page.repository_location
Page::Project::Show.act do
choose_repository_clone_ssh
repository_location
end end
end end
product :repository_http_location do attribute :repository_http_location do
Page::Project::Show.act do Page::Project::Show.perform do |page|
choose_repository_clone_http page.choose_repository_clone_http
repository_location page.repository_location
end end
end end
@ -37,7 +36,7 @@ module QA
def fabricate! def fabricate!
group.visit! group.visit!
Page::Group::Show.act { go_to_new_project } Page::Group::Show.perform(&:go_to_new_project)
Page::Project::New.perform do |page| Page::Project::New.perform do |page|
page.choose_test_namespace page.choose_test_namespace

View File

@ -6,14 +6,16 @@ module QA
class ProjectImportedFromGithub < Resource::Project class ProjectImportedFromGithub < Resource::Project
attr_writer :personal_access_token, :github_repository_path attr_writer :personal_access_token, :github_repository_path
dependency Factory::Resource::Group, as: :group attribute :group do
Factory::Resource::Group.fabricate!
end
product :name attribute :name
def fabricate! def fabricate!
group.visit! group.visit!
Page::Group::Show.act { go_to_new_project } Page::Group::Show.perform(&:go_to_new_project)
Page::Project::New.perform do |page| Page::Project::New.perform do |page|
page.go_to_import_project page.go_to_import_project

View File

@ -3,11 +3,12 @@ module QA
module Resource module Resource
class ProjectMilestone < Factory::Base class ProjectMilestone < Factory::Base
attr_accessor :description attr_accessor :description
attr_reader :title
dependency Factory::Resource::Project, as: :project attribute :project do
Factory::Resource::Project.fabricate!
end
product :title attribute :title
def title=(title) def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}" @title = "#{title}-#{SecureRandom.hex(4)}"
@ -17,12 +18,12 @@ module QA
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Menu.act do Page::Project::Menu.perform do |page|
click_issues page.click_issues
click_milestones page.click_milestones
end end
Page::Project::Milestone::Index.act { click_new_milestone } Page::Project::Milestone::Index.perform(&:click_new_milestone)
Page::Project::Milestone::New.perform do |milestone_new| Page::Project::Milestone::New.perform do |milestone_new|
milestone_new.set_title(@title) milestone_new.set_title(@title)

View File

@ -6,9 +6,11 @@ module QA
class Runner < Factory::Base class Runner < Factory::Base
attr_writer :name, :tags, :image attr_writer :name, :tags, :image
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-with-ci-cd' Factory::Resource::Project.fabricate! do |resource|
project.description = 'Project with CI/CD Pipelines' resource.name = 'project-with-ci-cd'
resource.description = 'Project with CI/CD Pipelines'
end
end end
def name def name
@ -26,7 +28,7 @@ module QA
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Menu.perform(&:click_ci_cd_settings)
Service::Runner.new(name).tap do |runner| Service::Runner.new(name).tap do |runner|
Page::Project::Settings::CICD.perform do |settings| Page::Project::Settings::CICD.perform do |settings|

View File

@ -8,17 +8,15 @@ module QA
class Sandbox < Factory::Base class Sandbox < Factory::Base
attr_reader :path attr_reader :path
product :id do attribute :id
true # We don't retrieve the Group ID when using the Browser UI attribute :path
end
product :path
def initialize def initialize
@path = Runtime::Namespace.sandbox_name @path = Runtime::Namespace.sandbox_name
end end
def fabricate! def fabricate!
Page::Main::Menu.act { go_to_groups } Page::Main::Menu.perform(&:go_to_groups)
Page::Dashboard::Groups.perform do |page| Page::Dashboard::Groups.perform do |page|
if page.has_group?(path) if page.has_group?(path)

View File

@ -4,15 +4,17 @@ module QA
class SecretVariable < Factory::Base class SecretVariable < Factory::Base
attr_accessor :key, :value attr_accessor :key, :value
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-with-secret-variables' Factory::Resource::Project.fabricate! do |resource|
project.description = 'project for adding secret variable test' resource.name = 'project-with-secret-variables'
resource.description = 'project for adding secret variable test'
end
end end
def fabricate! def fabricate!
project.visit! project.visit!
Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Menu.perform(&:click_ci_cd_settings)
Page::Project::Settings::CICD.perform do |setting| Page::Project::Settings::CICD.perform do |setting|
setting.expand_secret_variables do |page| setting.expand_secret_variables do |page|

View File

@ -6,21 +6,19 @@ module QA
class SSHKey < Factory::Base class SSHKey < Factory::Base
extend Forwardable extend Forwardable
attr_accessor :title
attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key attribute :private_key
product :title attribute :title
product :fingerprint attribute :fingerprint
def key def key
@key ||= Runtime::Key::RSA.new @key ||= Runtime::Key::RSA.new
end end
def fabricate! def fabricate!
Page::Main::Menu.act { go_to_profile_settings } Page::Main::Menu.perform(&:go_to_profile_settings)
Page::Profile::Menu.act { click_ssh_keys } Page::Profile::Menu.perform(&:click_ssh_keys)
Page::Profile::SSHKeys.perform do |page| Page::Profile::SSHKeys.perform do |page|
page.add_key(public_key, title) page.add_key(public_key, title)

View File

@ -5,7 +5,6 @@ module QA
module Resource module Resource
class User < Factory::Base class User < Factory::Base
attr_reader :unique_id attr_reader :unique_id
attr_writer :username, :password, :name, :email
def initialize def initialize
@unique_id = SecureRandom.hex(8) @unique_id = SecureRandom.hex(8)
@ -31,14 +30,14 @@ module QA
defined?(@username) && defined?(@password) defined?(@username) && defined?(@password)
end end
product :name attribute :name
product :username attribute :username
product :email attribute :email
product :password 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.act { has_personal_area?(wait: 0) } if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
Page::Main::Menu.perform { |main| main.sign_out } Page::Main::Menu.perform { |main| main.sign_out }
end end

View File

@ -4,9 +4,11 @@ module QA
class Wiki < Factory::Base class Wiki < Factory::Base
attr_accessor :title, :content, :message attr_accessor :title, :content, :message
dependency Factory::Resource::Project, as: :project do |project| attribute :project do
project.name = 'project-for-wikis' Factory::Resource::Project.fabricate! do |resource|
project.description = 'project for adding wikis' resource.name = 'project-for-wikis'
resource.description = 'project for adding wikis'
end
end end
def fabricate! def fabricate!

View File

@ -5,9 +5,9 @@ module QA
def fabricate!(*traits) def fabricate!(*traits)
raise ArgumentError unless traits.include?(:enabled) raise ArgumentError unless traits.include?(:enabled)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.perform(&:sign_in_using_credentials)
Page::Main::Menu.act { go_to_admin_area } Page::Main::Menu.perform(&:go_to_admin_area)
Page::Admin::Menu.act { go_to_repository_settings } Page::Admin::Menu.perform(&:go_to_repository_settings)
Page::Admin::Settings::Repository.perform do |setting| Page::Admin::Settings::Repository.perform do |setting|
setting.expand_repository_storage do |page| setting.expand_repository_storage do |page|
@ -16,7 +16,7 @@ module QA
end end
end end
QA::Page::Main::Menu.act { sign_out } QA::Page::Main::Menu.perform(&:sign_out)
end end
end end
end end

View File

@ -7,7 +7,7 @@ module QA
module Logger module Logger
extend SingleForwardable extend SingleForwardable
def_delegators :logger, :debug, :info, :error, :warn, :fatal, :unknown def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
singleton_class.module_eval do singleton_class.module_eval do
def logger def logger

View File

@ -49,11 +49,13 @@ module QA
cluster.install_prometheus = true cluster.install_prometheus = true
cluster.install_runner = true cluster.install_runner = true
end end
kubernetes_cluster.populate(:ingress_ip)
project.visit! project.visit!
Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |p| Page::Project::Settings::CICD.perform do |p|
p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") p.enable_auto_devops_with_domain(
"#{kubernetes_cluster.ingress_ip}.nip.io")
end end
project.visit! project.visit!

View File

@ -19,7 +19,7 @@ describe QA::Factory::Base do
before do before do
allow(subject).to receive(:current_url).and_return(product_location) allow(subject).to receive(:current_url).and_return(product_location)
allow(subject).to receive(:new).and_return(factory) allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product) allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product)
end end
end end
@ -115,73 +115,134 @@ describe QA::Factory::Base do
end end
end end
describe '.dependency' do shared_context 'simple factory' do
let(:dependency) { spy('dependency') }
before do
stub_const('Some::MyDependency', dependency)
end
subject do subject do
Class.new(described_class) do Class.new(QA::Factory::Base) do
dependency Some::MyDependency, as: :mydep do |factory| attribute :test do
factory.something! 'block'
end
attribute :no_block
def fabricate!
'any'
end
def self.current_url
'http://stub'
end end
end end
end end
it 'appends a new dependency and accessors' do let(:factory) { subject.new }
expect(subject.dependencies).to be_one end
describe '.attribute' do
include_context 'simple factory'
it 'appends new product attribute' do
expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
end end
it 'defines dependency accessors' do context 'when the product attribute is populated via a block' do
expect(subject.new).to respond_to :mydep, :mydep= it 'returns a fabrication product and defines factory attributes as its methods' do
end result = subject.fabricate!(factory: factory)
describe 'dependencies fabrication' do expect(result).to be_a(QA::Factory::Product)
let(:dependency) { double('dependency') } expect(result.test).to eq('block')
let(:instance) { spy('instance') }
subject do
Class.new(described_class) do
dependency Some::MyDependency, as: :mydep
end
end end
end
context 'when the product attribute is populated via the api' do
let(:api_resource) { { no_block: 'api' } }
before do before do
stub_const('Some::MyDependency', dependency) expect(factory).to receive(:api_resource).and_return(api_resource)
allow(subject).to receive(:new).and_return(instance)
allow(subject).to receive(:current_url).and_return(product_location)
allow(instance).to receive(:mydep).and_return(nil)
expect(QA::Factory::Product).to receive(:populate!)
end end
it 'builds all dependencies first' do it 'returns a fabrication product and defines factory attributes as its methods' do
expect(dependency).to receive(:fabricate!).once result = subject.fabricate!(factory: factory)
subject.fabricate! expect(result).to be_a(QA::Factory::Product)
expect(result.no_block).to eq('api')
end
context 'when the attribute also has a block in the factory' do
let(:api_resource) { { test: 'api_with_block' } }
before do
allow(QA::Runtime::Logger).to receive(:info)
end
it 'returns the api value and emits an INFO log entry' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('api_with_block')
expect(QA::Runtime::Logger)
.to have_received(:info).with(/api_with_block/)
end
end
end
context 'when the product attribute is populated via a factory attribute' do
before do
factory.test = 'value'
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('value')
end
context 'when the api also has such response' do
before do
allow(factory).to receive(:api_resource).and_return({ test: 'api' })
end
it 'returns the factory attribute for the product' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('value')
end
end
end
context 'when the product attribute has no value' do
it 'raises an error because no values could be found' do
result = subject.fabricate!(factory: factory)
expect { result.no_block }
.to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.")
end end
end end
end end
describe '.product' do describe '#web_url' do
include_context 'fabrication context' include_context 'simple factory'
subject do it 'sets #web_url to #current_url after fabrication' do
Class.new(described_class) do subject.fabricate!(factory: factory)
def fabricate!
"any"
end
product :token expect(factory.web_url).to eq(subject.current_url)
end end
end
describe '#visit!' do
include_context 'simple factory'
before do
allow(factory).to receive(:visit)
end end
it 'appends new product attribute' do it 'calls #visit with the underlying #web_url' do
expect(subject.attributes).to be_one factory.web_url = subject.current_url
expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute) factory.visit!
expect(subject.attributes[0].name).to eq(:token)
expect(factory).to have_received(:visit).with(subject.current_url)
end end
end end
end end

View File

@ -1,79 +0,0 @@
describe QA::Factory::Dependency do
let(:dependency) { spy('dependency' ) }
let(:factory) { spy('factory') }
let(:block) { spy('block') }
let(:signature) do
double('signature', name: :mydep, factory: dependency, block: block)
end
subject do
described_class.new(factory, signature)
end
describe '#overridden?' do
it 'returns true if factory has overridden dependency' do
allow(factory).to receive(:mydep).and_return('something')
expect(subject).to be_overridden
end
it 'returns false if dependency has not been overridden' do
allow(factory).to receive(:mydep).and_return(nil)
expect(subject).not_to be_overridden
end
end
describe '#build!' do
context 'when dependency has been overridden' do
before do
allow(subject).to receive(:overridden?).and_return(true)
end
it 'does not fabricate dependency' do
subject.build!
expect(dependency).not_to have_received(:fabricate!)
end
end
context 'when dependency has not been overridden' do
before do
allow(subject).to receive(:overridden?).and_return(false)
end
it 'fabricates dependency' do
subject.build!
expect(dependency).to have_received(:fabricate!)
end
it 'sets product in the factory' do
subject.build!
expect(factory).to have_received(:mydep=).with(dependency)
end
it 'calls given block with dependency factory and caller factory' do
expect(dependency).to receive(:fabricate!).and_yield(dependency)
subject.build!
expect(block).to have_received(:call).with(dependency, factory)
end
context 'with no block given' do
let(:signature) do
double('signature', name: :mydep, factory: dependency, block: nil)
end
it 'does not error' do
subject.build!
expect(dependency).to have_received(:fabricate!)
end
end
end
end
end

View File

@ -1,73 +1,21 @@
describe QA::Factory::Product do describe QA::Factory::Product do
let(:factory) do let(:factory) do
Class.new(QA::Factory::Base) do Class.new(QA::Factory::Base) do
def foo attribute :test do
'bar' 'block'
end end
attribute :no_block
end.new end.new
end end
let(:product) { spy('product') } let(:product) { spy('product') }
let(:product_location) { 'http://product_location' } let(:product_location) { 'http://product_location' }
subject { described_class.new(factory, product_location) } subject { described_class.new(factory) }
describe '.populate!' do before do
before do factory.web_url = product_location
expect(factory.class).to receive(:attributes).and_return(attributes)
end
context 'when the product attribute is populated via a block' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:test, proc { 'returned' })]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.test).to eq('returned')
end
end
context 'when the product attribute is populated via the api' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:test)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
expect(factory).to receive(:api_resource).and_return({ test: 'returned' })
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.test).to eq('returned')
end
end
context 'when the product attribute is populated via a factory attribute' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:foo)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.foo).to eq('bar')
end
end
context 'when the product attribute has no value' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:bar)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
expect { described_class.populate!(factory, product_location) }
.to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.")
end
end
end end
describe '.visit!' do describe '.visit!' do