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.
Support for UI testing is being worked on in another gem leap_salesforce_ui
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
sfdx
leveraging all of it's benefits - Supported by Sentify who can provide support and training to help you get started and overcome challenges
Table of Contents
- LeapSalesforce
Note this documentation is a work in progress. Look at
spec
for 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
Note there is a Leaps
alias that can be declared if you require leap_salesforce/leaps
which makes commands
shorter, meaning that Leaps
can be used instead of LeapSalesforce
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'
Security tokens can be set for each user (adding to password in OAuth like in this article) To do that set an environment variable as USER_KEY_token. E.g, so for a user with a key of :admin the token would be set with the
admin_token
environment variable
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_ENV
environment 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.
Setup scripts
The leap_salesforce init
runs 2 rake tasks on the initial suite after having created the basic files. These are:
leaps:create_soql_objects
This reads from the list of soql objects defined in.leap_salesforce.yml
and creates a class inheriting fromSoqlData
that maps to a backend object. A difference in name created from this script can be defined in that file with formatDesiredClassName:ActualClassName
.
E.g., Broker: Broker__c
will mean that a Ruby class called Broker
will be created that will map to the Broker__c
custom object.
A separate file with the ending _field_names
is also created from the metadata for each object. This creates
accessors for each field in the table with names that map from a Ruby friendly name to the Salesforce backend name.
leaps:create_enums
This task reads the soql objects defined in .leap_salesforce.yml
and creates a metadata/enum
folder that has
picklist information for all the picklist fields of each object. These are designed to be readonly as they will
be overridden when this command is run again.
These are designed to be used as:
- A reference when setting the value of a picklist. Rather than
contact.lead_source = 'Web'
one can use
contact.lead_source = Contact::LeadSource.web
which makes it clearer that a picklist value is used (not just free text) and also have a value that can be
refactored if there is a change in the picklist.
- A method of detecting changes in picklist values Sometimes due to older methods of deployment (or a disconnect between dev and testing team), a picklist value may change without the test team knowing about it (or a known change needs to be verified). Since the history of these values can be tracked when these files are committed to source control, a change can be detected when a test like the following is run
RSpec.describe 'Picklists' do
LeapSalesforce.objects_to_verify.each do |data_class|
SoqlEnum.values_for(data_class).each do |picklist|
it "#{picklist} has not changed values" do
expect(data_class.picklist_for(picklist.name)).to match_array picklist.values
end
end
end
end
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/
Advanced
Topics in here for understanding how things work behind the scenes and are not necessary for simple use of this gem.
Understanding how request is built
Creating entities
When creating entities, the convention is that the accessors defined by the soql_element
that are
auto-generated by the leaps:create_soql_objects
rake task are used.
These methods internally call the []=
method to set values that will be sent in the POST request.
This method can be used directly with the backend name. E.g, contact[:FieldName] = 'Value'
This method in turn sets a value of the @override_parameters[:body]
variable within the entity.
The value of request body can be interrogated with entity.request_parameters.body
.
Logging
By default, API traffic will be logged in a log file in a logs
folder. The gem soaspec
is
used to log this traffic.
Following is an example of changing some of the default logging.
# Turn this true if you need debug authentication
Soaspec::OAuth2.debug_oauth = true
# Turn this to true if you want to see API traffic on the terminal
Soaspec::SpecLogger.output_to_terminal = true
See more configuration parameters in the Soaspec repo
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
Composite creation
When creating bulk data (e.g, data with related fields), it's faster to use the composite API.
See spec/integration/composite_spec.rb
for an example of how to do this. This will be built into
this gem more in the future (so that composite requests are made automatically).
Following is an example of the time difference for just putting 2 requests into one request.
Create Account: 0.96s Create Contact linked to this account: 0.75s Total: 1.7s
Create Contact and account using Composite: 1.24s
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
- returnstrue
orfalse
indicating 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 (this folder can be configured in .leap_salesforce.yml)
│ ├── 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'
│ ├── {object_name} # Class mapping to a Soql object
│ └── {object_name}_field_names # Field name accessors auto created/updated based no metadata in Salesforce
├── 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.