LeapSalesforce
Welcome to LeapSalesforce gem. This gem helps ones to perform integration tests on Salesforce. It reads the Metadata from Salesforce and creates the foundation for API tests.
In the future it will also planned to be used to create page objects based on metadata to support UI testing.
Is this for you?
This gem is an open source library aimed at making integrated test automation
for Salesforce easy. Using it does require you to use code so if you really must
have a codeless automation suite then you will need another tool
(e.g., Provar). However if you have some engineers (dev
or test) who are keen to get their hands into some code this is worth trying.
The benefits of an open source tool like this are:
- Ease of use. Initialisation scripts make getting setup easy, code examples demonstrate how to perform common actions
- Transparency. Nothing is hidden. Every piece of code can be seen by you
- Flexibility. Using Ruby you are able to customize and extend the code however you like and if you want to share it, you can easily do so with a pull request
- Mutual growth. The hope is with many using this library, test automation engineers can support each other and mutually create a library that makes common test cases easy
- It itself is unit tested. You can have confidence of all the features shown and can add extra unit tests if you need more confidence
- Built with and for CI in Gitlab. Designed to work within Docker containers
- Will integrate with
sfdxleveraging all of it's benefits - Supported by IntegrationQA who can provide support and training to help you get started and overcome challenges
Table of Contents
Note this documentation is a work in progress. Look at
specfor code examples.
Installation
Add this line to your application's Gemfile:
gem 'leap_salesforce'
And then execute:
$ bundle
Or install it yourself as:
$ gem install leap_salesforce
Usage
Getting started
After installing the gem, to get started in creating a fresh repository, the leap_salesforce executable can be used.
It will ask for credentials and setup files that will locally store them.
This assumes that a Salesforce OAuth app be set up to be used for the API which can be done by
following this wiki.
E.g
leap_salesforce init
Credentials are not stored in stored in source control. They can be setting through the following environment variables:
- 'client_id'
- 'client_secret'
- 'password'
Tests can be run using the default Rake task.
E.g.,
rake # Run all tests
API Traffic logs can be seen in the logs folder. You can see an example of running through
this here.
Understanding how things work
This section details what the most important files are, how to define test users and how to create, read, update, and delete data.
Important files
To see how things fit together, look at the structure section below.
.leap_salesforce.yml
This YAML file describes common configuration for the project.
Following is a description of each key in this file:
environment: Specifies the default environment for this suite. This can be overwritten with theLEAP_ENVenvironment variable.lib_folder: Can be set to change the default location (lib/leap_salesforce) of where all generated code is put and read from.soql_objects: List of SOQL objects that the generator will create for and updatesfdx: Boolean for whether to use sfdx for authentication. Defaults to false
salesforce_oauth2.yml
This file is used for using your own OAuth application. See next section for SFDX
- client_id: OAuth2 client id / customer id obtained from your Test App
- client_secret: OAuth2 client_secret / customer secret obtained from your Test App
- password: Password expected to be generic across test users and the same as what's used on the UI to logon with
This file is read and sets attributes of LeapSalesforce globally. These can also be set with the following format
LeapSalesforce.password = 'PASS'
The approach using these credentials follows this tutorial
config/general.rb
This is where common code is stored for all the environments. This is where you would usually put your test users as described in the next section.
For sfdx, set the ENV['SF_USERNAME'] to the user to login to sfdx with and create a server.key for
authentication. How sfdx will fully work with different user roles is still a work in progress.
Changing environment
The LEAP_ENV environment variable can be used to change environment in non sfdx mode. The default is set through
.leap_salesforce.yml environment key.
In sfdx mode, setting env variable SCRATCH_ORG to true
will tell leap salesforce to authenticate against a SCRATCH ORG. It will attempt to use either the SCRATCH_ORG_ALIAS
environment variable.
Alternatively, set the environment variables SCRATCH_INSTANCE_URL and SCRATCH_ACCESS_TOKEN from sfdx:org:display
to enable tests to use that to authenticate to environment directly.
Test Users
Test users are defined using the LeapSalesforce::Users module. Following is an example of setting up a few test
users:
module LeapSalesforce
# Example where email address changes according to environment
# Users can be added by passing an array or passing a LeapSalesforce::User object
Users.add [:admin, 'admin@<%= LeapSalesforce.environment %>.email.com', description: 'System Admin User']
Users.add User.new :sales, 'test.sales@test<%= LeapSalesforce.environment %>.com'
end
The first user defined will be the default user. Following users can be set by using the api_user attribute.
# Using key to specify user
LeapSalesforce.api_user = LeapSalesforce::Users.where(key: :sales)
# Using username that has a partial match with a Regex
LeapSalesforce.api_user = LeapSalesforce::Users.where username: /admin/
# Using description that has a partial match with a Regex. This might be helpful if you're setting users from
# a Cucumber step definition where readability is important
LeapSalesforce.api_user = LeapSalesforce::Users.where description: /System Admin/
CRUD of data
To work data in Salesforce, an object inheriting from the SoqlData class is always used. The idea is
that an object in Ruby code maps to the object in Salesforce and requests and updates to this object
are reflected in Salesforce.
When the initialisation script is run, it creates such classes in a folder called soql_data.
Following a simple example of a class representing the 'ContentDocument' object in Salesforce. It also requires a generated file that specifies accessors to set and retrieve information about the object.
require_relative 'document_field_names'
# An Document object mapping to a SOQL ContentDocument
class Document < SoqlData
include Document::Fields
soql_object 'ContentDocument'
end
For all interactions with Salesforce the API traffic logs are recorded in a log created in the logs
folder of the suite.
Creating entities
There are several ways entities can be created. By instantiating the object with the new method a new
object will be created in memory but in Salesforce. Only when the save! method is called will an object
be created.
For example
@contact = Contact.new # Create an object in memory
@contact.last_name = 'Test Person' # Set the last name field of that object to 'Test Person'
@contact.save! # Calls Salesforce API to create a new object with the fields set in the object
The log for this call will look like the following:
(Note that explanations are in ALL CAPS and line numbers have been added)
1. Leaps, [13:39:14] : Example Factory for 'Contact'
2. Leaps, [13:39:14] : request body: {"LastName":"Gleichner"}
3. Leaps, [13:39:14] : RestClient.post "https://SALESFORCE_INSTANCE_URL/services/data/v45.0/sobjects/Contact", "{\"FirstName\":\"Lewis\"}", HEADERS INFO
4. Leaps, [13:39:16] : # => 201 Created | application/json 71 bytes
5. Leaps, [13:39:16] : response:
6. headers: {:date=>"Thu, 01 Aug 2019 01:39:15 GMT", OTHER_HEADER_INFO}
7. body: {"id":"0032v00002qU3hvAAC","success":true,"errors":[]}
Following is a step by step explanation of each log line:
- Brief description of what's being done. Creating a Contact.
- Parameters used in HTTP request. This will be the fields set on the object created
- The actual REST Post made to the Salesforce API showing the URL, payload, headers, etc
- A brief summary of the response. Object has been created successfully
- A more in depth description of the response follows in next two lines
- Headers of the response
- Body of the response
Representation of the details for created an entity can be handled much neater by FactoryBot.
If we had the following factory for Contact
FactoryBot.define do
factory :contact do
last_name { 'Test Person' }
end
end
then we could perform the same creation of a contact with simply
@contact = FactoryBot.create(:contact)
FactoryBot has traits, associations, after blocks for helping with creating objects with fast number
of relationships. See FactoryBot's getting started for more information and have a look at the
examples in the spec folder.
To create an object using a factory, the create method can also be used on the object itself. For
example:
@contact = Contact.create
Reading entities
Retrieving entities
To retrieve an entity, the find method can be called on the class for the object required. For example
to obtain an object representing a contact with a last name of 'Test Person' we could do:
@contact = Contact.find last_name: 'Test Person'
This uses the ruby friendly method defined for Contact and shown in Contact::Fields to extract
the Salesforce field name LastName. Note, the name designated is derived from the label name which
is assumed to be more user friendly than the backend name. See spec/unit/ext/string_spec.rb for
examples of how different behaviours are handled.
The backend name can also be used directly within a find so
the following could also be done:
@contact = Contact.find LastName: 'Test Person'
The values used in these requests are validated against Metadata before the request is made so an error will be received if a field name is used that does not exist on the object.
Any number of parameters can be passed to the find method to narrow the search. For example:
@contact = Contact.find last_name: 'Test Person', first_name: 'Number 1'
When a date is passed as the value it will be automatically formatted so that it can be used in the backend SOQL query. So to get a contact created less than 5 days ago one can use:
@contact = Contact.find created_date: "<#{5.days.ago}"
To use the LIKE operator for partial matches put a '`' at the start of the string. Following is an
example of finding a contact that has the string 'Test' anywhere in their first name. As one can see
the '%' symbols are used as a wild card to indicate any value.
@contact = Contact.find(first_name: '~%Test%')
Note for non unique criterion, the value returned will be the most recent one.
Retrieving the value of a field
Getters (methods that retrieve something about an object) are created for each field name on an object.
So to get the first name of the contact, the first_name method is simply called on it.
@contact = Contact.find first_name: 'Test Person'
@contact.first_name # => 'Test Person'
The backend name can also be used in the [] method to retrieve a value.
@contact['FirstName'] # => 'Test Person'
There are 2 special methods related to verifying that a response is successful or not.
success- returnstrueorfalseindicating whether the previous action performed worked as expectederror_message- returns a string with the error message returned from Salesforce.
For example, say you want to test that a LineItem cannot be deleted. You can verify it and it's
error message with:
item = LineItem.find status: 'New'
expect(item.delete.).to eq 'Deleting of line item is not allowed.'
Updating entities
The same field name used above is used as a setter to update an individual field. For example, to change the first name of a contact from Test1 to Test2:
@contact = Contact.find first_name: 'Test1'
@contact.first_name = 'Test2'
To update multiple fields at once, use the update method:
@case.update status: Case::Status.escalated, case_reason: 'Feedback'
This does not fail if the update is not successful. To fail in this situation, the success_update
method can be used:
@case.success_update status: Case::Status.escalated, case_reason: 'Feedback'
If the main action of the test is this update, it is recommended that success be verified explicitly with:
@case.update status: Case::Status.escalated, case_reason: 'Feedback'
expect(@case).to be_successful
The reason for this is to account for error scenarios where you want to update a value and expect and error message. For example:
item = LineItem.find(status: 'New')
update = item.update owner_id: User.find(Name: '~%Confidential%').id
expect(update.).to eq 'Cannot change owner to confidential user'
Deleting entities
Once an entity is obtained through find or create, it can be deleted simply with the
delete. If you want an exception to be raised if the delete fails, pass must_pass: true to the
delete method. For example:
@contact = Contact.find first_name 'No longer needed'
@contact.delete must_pass: true
An entity can also be deleted with merely it's id. For example:
Contact.delete '0032v00002rgv2pAAA', must_pass: true
Other Examples
See spec/integration folder for examples of tests.
Delete old contacts
# Deleting old contacts
objects = Contact.each_id_with created_date: "<#{5.days.ago}"
puts objects.count # Log how many are being deleted
objects.each do |id|
puts "Deleting #{id}"
Contact.delete id
end
Structure
Following is the general structure of test automation suite that uses this approach. Details may vary depending on the test framework used and other preferences.
.
├── config # Code for the configuration of the automation suite
│ ├── general.rb # Code loaded for common (non confidential code) setup across environments
│ ├── credentials # Setting of secret properties like passwords
│ │ └── salesforce_oauth.yml # Credentials for connecting to Salesforce via OAuth2.
│ └── environments # Contains ruby files loaded specific to environment following `ENVIRONMENT_NAME.rb`
├── lib # Common library code
│ └── leap_salesforce # Code generated by or specific to leap_salesforce
│ ├── factories # FactoryBot definitions, describing how to mass produce objects
│ ├── metadata # Code generated and updated automatically from metadata
│ │ └── enum # Picklist enumeration objects are stored here
│ └── soql_data # Objects for handling each object in the backend specified in '.leap_salesforce.yml'
├── logs # Contains API traffic logs for transactions against Salesforce
├── spec # Where RSpec automated tests are stored
├── .leap_salesforce.yml # Where common configuration is stored regarding your project. This complements and is read before what's in 'config'
├── Gemfile # Where required ruby gems/libraries are specified
├── Gemfile.lock # Generated file specified details of versions installed by `Gemfile`
└── Rakefile # Where common `Rake` tasks are specified. LeapSalesforce specific tasks are required from here
Docs
Technical docs here
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/leap-dojo/leap_salesforce. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the LeapSalesforce project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
References
See the presentation on this here