How to prepare a test environment without going crazy

Next, I'll tell you how we eventually reduced this process to 1 teams in the Rails console.

What exactly is the problem?

While the project was small, the entire setup of the test environment essentially boiled down to issuing the current user a unique identifier tied to a specific status:

  • registered and has a positive balance;

  • registered, balance 0;

  • not registered (registration available);

  • blocked (registration unavailable);

The project was launched in mid-2022, until this time users could write off and accumulate bonuses only with Sberbank cards, then – with any cards and with different payment methods (online card, courier, etc.) Over the past year, the project has acquired new features:

  1. It became possible to write off up to 99% bonuses at certain retailers (at the start of the project there was only up to 10%).

  2. Special promotions such as:

    • accruals for alcohol – previously the code worked in such a way that if the order contained alcohol, any transactions with bonuses were prohibited;

    • progressive cashback for subscribers – accruals increase in proportion to the order amount.

  3. Accruals for orders were delayed for a day (grocery orders) or 15 days (electronics) to reduce the risk of fraud.

For safe implementation, many changes were activated by feature flags, the role model grew (some settings were available to employees with certain roles), and there were even more features. All this increased the number of steps to prepare a test bench.

Let me give you an example: in order to run the task “Deferred accruals at retailer A” on the stand, you had to:

  1. Give the user a registered identifier.

  2. Activate the delayed_accruals feature flag.

  3. Assign the user the necessary roles, for example, to carry out order returns.

  4. Create the necessary promotions at your stand.

  5. Add retailer A to the config, where the ids of retailers for which accruals will work deferred are stored.

  6. Set the time in minutes after which the accrual should work (for sales this is 1440, for a stand 2 minutes is enough).

  7. Set up a banner at checkout that will warn the user that bonuses for the current order will be credited after N time.

  8. Set up 99% write-off at retailer A.

  9. Give the user a subscription.

Total, 9 actions for each deployment (actually, setting up a test environment). Next, we needed to create several test orders:

  1. Log in to the site.

  2. Add items to cart.

  3. At checkout, select the delivery time, replacement policy, payment method, delivery address, etc.

  4. Place an order (at this stage money and bonuses are held)

    Next, it was necessary to bring this test order to the “Delivered” status so that the job that is responsible for calculating bonuses would be scheduled for execution in N time:

    • first the order is taken into assembly;

    • then all the goods in the order are taken, they are given the status “Assembled”, the assembly is completed;

    • Then the final payment for the order and debiting of bonuses occurs;

    • after the order is “delivered” and the accrual job is launched.

Setting up could easily take up to half an hour, and if you check 3-4 tasks a day, then up to two hours. It happened that I spent half an hour setting up the environment in order to test a task for ten minutes.

My team and I realized that we couldn’t live like this any longer, and we decided to write code wrapped in a rake task that would allow us to automate routine actions.

Code that solves the problem

Our code is needed to set up a test environment, but will be in production so that it can be available at any stand to any team. Therefore, the first thing we did was write a check that would prohibit execution of the command in the production environment and allow it in the test environment:

return Failure(message: "No way you're doing it in production") if 
Rails.env.production?

Next, we announced a list of feature flags that need to be enabled each time the command is called:

FEATURES_LIST = %i[
  flag1 
  flag2 
  flagN
].freeze

Next, we described the methods separately for each required action. We activate feature flags from the list:

def enable_flags
  FEATURES_LIST.each { |feature| FeatureToggle.enable!(feature) }
end

We issue the user with the required roles (if the point of the task is not to check the role system, you can issue all existing ones).

def add_roles(user)
  user.roles = Role.all
end

We set the user’s identifier in the loyalty program (registered), having previously deleted all old ones, if any.

def update_auth(user)
  user.user_authentications.destroy_all
  user.user_authentications.create!(
    provider: 'provider_name',
    uid: external_service_id,
    payload: { uid: external_service_id }
  )
end

We create a basic promotion (and others if necessary):

def create_basic_promotion!
  Loyalty::AccrualPromotion.find_or_create_by!(kind: 'basic') do |promo|
    promo.name="Базовое начисление"
    promo.calculator="AccrualCalculator"
    promo.state="active"
    promo.percent = 1
    promo.display_name="Базовое"
  end
end

We set the write-off percentage for the first retailer:

def max_spend_percent_for_first_retailer
  ChargeSettings.create!(
    starts_at: 1.year.ago,
    percent: 99,
    retailer_id: Retailer.first.id
  )
end
As a result, the code looks like this:
FEATURES_LIST = %i[
  flag1 
  flag2 
  flagN
].freeze


option :user_id
option :loyalty_id


def call
  return Failure(message: "No way you're doing it in production") if Rails.env.production?


  user = Spree::User.find(user_id)
  enable_flags
  add_roles(user)
  update_auth(user)
  create_basic_promotion!
  max_spend_percent_for_first_retailer
  Success(message: "Loyalty is enabled! user id = #{user_id}; service id = #{loyalty_id}")
end

How to prepare a test environment with one command

Next, I will tell you how to use the written code in practice, where you can call it from, and also how to connect to the rails console faster.

You can call the EnableForTesting class in the test environment in 2 ways:

  • from rails console;

  • from bash.

To quickly connect to the rails console or bash we use kubectl + kch script, for which special thanks to my colleague Alexey. The script is a set of aliases for our frequently used kubectl commands.

The command is called in the format kch namespace pod cmd. For example, instead of a long command kubectl exec -n stand-0000 -i -t -c app api-pod-ffffff-0000 -- rails c you can write a short one kch 3003 api rails, which will be transformed into the long one described above, i.e. we don’t need to know the full names of the application pods – just know the stand number and where we want to connect (rails, bash, mysql).

The script in the general kubectl list will find the full name of the namespace and the engine in it using the corresponding occurrences, substitute them into the command template to be called, and then execute the resulting command.

Called from bash with the command:

rake 'loyalty:enable_for_testing[user_id, loyalty_id]'

Call from rails console:

Loyalties::Tasks::EnableForTesting.call(user_id: 12345, loyalty_id: 'abcdef')

After calling the class for the user with id = 12345, all commands for setting up the test environment will be executed sequentially. The second argument (loyalty_id) is optional: if you do not pass it, the default loyalty_id will be set (with the status registered). If a different status is needed, we pass the required loyalty_id and it is tied to the test user.

We watch how commands are executed in the console, and we feel like a hacker.

After execution we see the result:

=> Success({:message=>"Loyalty is enabled! user id = 12345; loyalty_id ="abcdef"})

Done, we just saved half an hour of life 🙂

Conclusion

To create test orders, we also wrote code that can be executed in the console (all that remains is to wrap it in a separate rake task).

We also make sure to write an article in the internal knowledge base on how to use our rake tasks, so that QA from other teams can set everything up themselves if they need to check some functionality with bonuses. This reduces communication time.

Now setup takes less than a minute, and QA directly checks the code. When new features appear (and therefore feature flags and other artifacts), I make a merge request into the project and add the necessary code, keeping the tool up to date. Yes, we also deploy QA to production 🙂

In conclusion, I would like to say that the example described above can be transferred to another stack and business logic; of course, it will have its own implementation features, but the very idea of ​​​​using scripts to set up a test environment at test benches remains relevant.

Share how you set up a test environment in your teams?

SberMarket's tech team manages social networks with news and announcements. If you want to know what's under the hood of high-load e-commerce, follow us on Telegram and on YouTube. And also listen podcast “For tech and these” from our it managers.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *