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

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 the LEAP_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 update
  • sfdx: 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 from SoqlData that maps to a backend object. A difference in name created from this script can be defined in that file with format DesiredClassName: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:

  1. 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.

  1. 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:

  1. Brief description of what's being done. Creating a Contact.
  2. Parameters used in HTTP request. This will be the fields set on the object created
  3. The actual REST Post made to the Salesforce API showing the URL, payload, headers, etc
  4. A brief summary of the response. Object has been created successfully
  5. A more in depth description of the response follows in next two lines
  6. Headers of the response
  7. 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 - returns true or false indicating whether the previous action performed worked as expected
  • error_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.error_message).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.error_message).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.

References

  • Presentation on this library here
  • Example of this library within a CI/CD pipeline here
  • Using leap_salesforce to download event log files here
  • Video walking through setting up automation using this here