Skip to main content
This tutorial demonstrates how Vers transforms traditional testing workflows by enabling parallel test execution from branched VM states. We’ll build a realistic e-commerce testing suite that showcases the core value of Vers: eliminating repetitive setup work through VM branching.

What You’ll Learn

  • How to set up a custom testing environment with Puppeteer
  • The power of branching VM states at decision points
  • Parallel test execution across different scenarios
  • How to save time compared to traditional testing workflows

Prerequisites

  • Vers CLI installed and authenticated
  • Basic familiarity with Node.js and testing concepts

Project Overview

We’ll test different payment flows on an e-commerce site. Instead of repeating the “navigate → add to cart → checkout” steps for each payment method, we’ll:
  1. Set up once: Create a VM with Puppeteer and dependencies
  2. Branch at decision points: Capture the state right before payment selection
  3. Test in parallel: Run different payment scenarios simultaneously
This approach saves significant time as your test suite grows.

Step 1: Project Setup

Initialize the Project

mkdir ecommerce-testing
cd ecommerce-testing
vers init

Configure the Environment

The vers init command generates vers.toml, which holds metadata about the cluster you are about to create. Edit the generated vers.toml to allocate sufficient resources:
[machine]
mem_size_mib = 1024         # Increased for browser testing
vcpu_count = 1
fs_size_cluster_mib = 6000  # Increased for browser testing
fs_size_vm_mib = 3000       # More space for dependencies

[rootfs]
name = "nodejs-puppeteer"

[kernel]
name = "default.bin"

[builder]
name = "docker"
dockerfile = "Dockerfile"
The larger disk and memory allocations are essential for Puppeteer’s dependencies and Chrome browser installation and usage.

Create the Dockerfile

Vers uses the Dockerfile format to determine which dependencies to allocate to your cluster. Under the hood, we translate it into our own format, without using Docker directly. Create a Dockerfile with all necessary dependencies:
FROM node:18-slim

# Install Puppeteer dependencies and SSH server
RUN apt-get update && apt-get install -y \
    chromium \
    curl \
    fonts-liberation \
    iproute2 \
    libasound2 \
    libatk-bridge2.0-0 \
    libdrm2 \
    libgtk-3-0 \
    libgtk-4-1 \
    libnss3 \
    libx11-xcb1 \
    libxcomposite1 \
    libxdamage1 \
    libxrandr2 \
    openssh-server \
    xdg-utils \
    emacs \
    vim \
    && rm -rf /var/lib/apt/lists/*

# Set up working directory
WORKDIR /app

# SSH server setup
RUN mkdir -p /var/run/sshd /run/sshd
RUN echo 'root:password' | chpasswd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

# Generate SSH host keys
RUN ssh-keygen -A

# Configure nameserver for internet access
RUN echo 'nameserver 8.8.8.8' > /etc/resolv.conf

# Install Puppeteer
RUN npm install puppeteer

# Set Puppeteer to use installed Chromium
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium

# Expose SSH port
EXPOSE 22

# Copy test files
COPY . .

# Start SSH service and keep container running
CMD service ssh start && tail -f /dev/null
The SSH server setup, iproute2 package, and nameserver configuration are required for Vers to function properly. Without these, you won’t be able to connect to your VMs or access the internet for package installations.
Let’s pause and walk through the configuration we just set up for our new cluster.
  1. We install necessary dependencies, including iproute2, which you will need for Vers to function properly on every cluster you build.
  2. Set up an ssh server. Without it, you won’t be able to connect to your cluster.
  3. We added DNS configuration. Note the following line:
RUN echo 'nameserver 8.8.8.8' > /etc/resolv.conf
Your cluster will NOT include DNS configuration by default. By adding nameserver 8.8.8.8 to /etc/resolv.conf, we’re configuring it to use Google’s public DNS.

Step 2: Launch and Set Up the Environment

Start the Cluster

vers build
vers run
The vers build command will build your custom rootfs with Puppeteer. vers run will
  1. Start a cluster with your new environment
  2. Create the root VM
vers build will only build your rootfs if vers.toml’s builder.dockerfile points to the Dockerfile you created and has the builder.name property set to docker like we configured above.

Connect and Set Up Dependencies

vers connect
Inside the VM, create your test dependencies: First, test internet connectivity.
curl -s https://httpbin.org/ip
The above command should return something like:
{
  "origin": "13.219.19.157"
}
If you do not see any output, configure your nameserver manually.
echo 'nameserver 8.8.8.8' > /etc/resolv.conf
Create a package.json file to define your Node.js project dependencies. This file tells npm what packages to install and provides metadata about your testing project:
Tip: In a virtual machine, you can create files using your preferred text editor (like vim or emacs which are installed), or use shell commands. For example, to create a file with content, you could use: cat > filename.ext << 'EOF' followed by your content, then EOF on a new line.
{
  "name": "ecommerce-testing",
  "version": "1.0.0",
  "description": "E-commerce testing with Puppeteer and Vers",
  "main": "index.js",
  "dependencies": {
    "puppeteer": "^21.0.0"
  },
  "keywords": ["testing", "automation", "puppeteer"],
  "author": "",
  "license": "ISC"
}
Install the dependencies (this may take a few minutes as Puppeteer downloads Chromium):
npm install puppeteer@latest --verbose

Step 3: Create the Base Test

Build the Core Test Infrastructure

Create a test-base.js file containing a reusable test class. This class handles browser setup, navigation, and common e-commerce actions that all your tests will share:
const puppeteer = require("puppeteer");

class ECommerceTest {
  constructor() {
    this.browser = null;
    this.page = null;
  }

  async setup() {
    this.browser = await puppeteer.launch({
      headless: true,
      args: ["--no-sandbox", "--disable-setuid-sandbox"],
    });
    this.page = await this.browser.newPage();
    await this.page.setViewport({ width: 1280, height: 720 });
  }

  async navigateToStore() {
    await this.page.goto("https://automationexercise.com");
    await this.page.waitForSelector(".features_items");
    console.log("✓ Navigated to Automation Exercise store");
  }

  async addItemToCart() {
    // Add item to cart using robust selector
    console.log("Looking for add to cart buttons...");
    await this.page.waitForSelector(".add-to-cart", { visible: true });

    await this.page.click(".add-to-cart");
    console.log("✓ Clicked add to cart");

    // Handle the modal
    await this.page.waitForSelector(".modal-content", { visible: true });
    await this.page.click(".btn-success.close-modal");
    console.log("✓ Added product to cart");
  }

  async goToCheckout() {
    // Go to cart
    await this.page.click('a[href="/view_cart"]');
    console.log("✓ Navigated to cart page");
    // Go to checkout
    await this.page.waitForSelector(".btn-default.check_out", {
      visible: true,
    });
    await this.page.click(".btn-default.check_out");
  }

  async cleanup() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

module.exports = ECommerceTest;

Create the Setup Test

Create a checkout-form.js file that demonstrates the core workflow and establishes our branching point. This script navigates through the complete purchase flow up to the point where different payment methods would be selected:
const ECommerceTest = require("./test-base");

async function testCheckoutForm() {
  const test = new ECommerceTest();
  await test.setup();

  try {
    await test.navigateToStore();

    await test.addItemToCart();

    await test.goToCheckout();

    // Perfect branching point!
    console.log("✓ Reached checkout - login/register required");
    console.log("✓ Perfect state for branching different user scenarios");

    // Keep the session alive for demonstration
    console.log("Keeping session active for state capture...");
    await new Promise((resolve) => setTimeout(resolve, 5000));
  } catch (error) {
    console.error("Test failed:", error.message);
  } finally {
    await test.cleanup();
  }
}

if (require.main === module) {
  testCheckoutForm();
}
Run the setup test to verify everything works:
node checkout-form.js
You should see output like:
✓ Navigated to Automation Exercise store
Looking for add to cart buttons...
✓ Clicked add to cart
✓ Added product to cart
✓ Navigated to cart page
✓ Reached checkout - login/register required
✓ Perfect state for branching different user scenarios

Step 4: Create the Branch Point

This is where Vers shines! Instead of repeating the setup for each test scenario, we’ll branch from this state.

Commit Your Changes

# Exit the VM
exit

# Create a commit at this decision point
vers commit --tag "Cart filled, ready for payment testing"

Create Credit Card Testing Branch

We’ll be testing two payment flows:
  1. Paying for items using a credit card.
  2. Using PayPal.
Naturally, each of these flows corresponds to a branch we’ll create in our cluster (more on that below). First, we’ll create a parent branch that represents our payment flow. Next, we’ll create a child branch for the credit card payment testing flow.
# Create and switch to credit card branch
vers checkout -c credit-card-payment

# Create a child VM for credit card testing
vers branch --name credit-card-payment-test
At this point, we’ve created two new branches. Each branch represents a VM in our cluster, inheriting the same dependencies, files, and running processes as its parent. As you’ll see, this makes starting new workflows much faster! Connect to the child VM from the last vers branch command output.
Use the actual VM ID from your vers branch output. Each VM gets a unique identifier.
vers connect f896077c-be4a-40af-869d-fdd33a8c2689

Create Credit Card Test

Create a credit-card-test.js file that simulates a credit card payment workflow:
const puppeteer = require("puppeteer");

async function testCreditCardPayment() {
  console.log("🔄 Starting Credit Card Payment Test...");

  const browser = await puppeteer.launch({
    headless: true,
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  const page = await browser.newPage();

  try {
    await page.goto("https://automationexercise.com");
    console.log("✓ Navigated to store (credit card branch)");

    // Add item to cart
    await page.waitForSelector(".add-to-cart");
    await page.click(".add-to-cart");
    console.log("✓ Added item to cart");

    // Simulate credit card payment flow
    await new Promise((resolve) => setTimeout(resolve, 2000));
    console.log("✓ Credit card payment form filled");
    console.log("✓ Credit card payment test completed successfully");

    console.log("💳 Credit card payment scenario tested");
  } catch (error) {
    console.error("❌ Credit card test failed:", error.message);
  } finally {
    await browser.close();
    console.log("✓ Credit card test browser closed");
  }
}

testCreditCardPayment();
Run the credit card test:
node credit-card-test.js

Create PayPal Testing Branch

For our final branch, we’ll simulate testing a PayPal checkout flow.
# Exit current VM
exit

# Create PayPal branch from the same base state
vers branch --name paypal-payment-test

# Switch to PayPal branch
vers checkout paypal-payment-test

# Connect to PayPal VM
# Be sure to use the VM ID from the above vers branch output
vers connect 64f09b63-b93a-4abd-9726-56f352ab3d44

Create PayPal Test

const puppeteer = require('puppeteer');

async function testPayPalPayment() {
  console.log('🔄 Starting PayPal Payment Test...');

  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const page = await browser.newPage();

  try {
    await page.goto('https://automationexercise.com');
    console.log('✓ Navigated to store (PayPal branch)');

    // Add item to cart
    await page.waitForSelector('.add-to-cart');
    await page.click('.add-to-cart');
    console.log('✓ Added item to cart');

    // Simulate PayPal payment flow
    await new Promise(resolve => setTimeout(resolve, 2000));
    console.log('✓ PayPal payment button clicked');
    console.log('✓ PayPal authentication simulated');
    console.log('✓ PayPal payment test completed successfully');

    console.log('💰 PayPal payment scenario tested');

  } catch (error) {
    console.error('❌ PayPal test failed:', error.message);
  } finally {
    await browser.close();
    console.log('✓ PayPal test browser closed');
  }
}

testPayPalPayment();
Finally, run the test!
node paypal-test.js

Step 6: Visualize and Execute

View Your VM Tree

Vers provides some handy commands to visualize the status of our cluster.
# Exit the PayPal payment flow testing VM
exit

# See the branching structure
vers tree
You’ll see output like:
Cluster: rGg3SyPR2dFV4g9BEK7a9A (Total VMs: 3)
└──  [P] 4aa0d7f7...84507010 (192.168.0.243)
    ├──  [R] f896077c...fdd33a8c (192.168.0.247)
    └──  [R] 64f09b63...56f352ab (192.168.0.249) <- HEAD
This shows:
  • Root VM [P]: Paused parent with the base checkout state
  • Credit Card VM [R]: Running child for credit card tests
  • PayPal VM [R]: Running child for PayPal tests

Execute Tests Non-Interactively

# Run PayPal test without connecting
vers execute "node paypal-test.js"

# Switch to credit card branch and test
vers checkout credit-card-payment-test
vers execute "node credit-card-test.js"
The vers execute command runs tests directly without opening an interactive session.

Step 7: Advanced Workflows

Parallel Testing

You can run tests simultaneously by opening multiple terminals: Terminal 1:
cd ecommerce-testing
vers checkout credit-card-payment-test
vers execute "node credit-card-test.js"
Terminal 2:
cd ecommerce-testing
vers checkout paypal-payment-test
vers execute "node paypal-test.js"
Both tests run in parallel from the same starting state!

Commit Test Results

# Commit PayPal test results
vers checkout paypal-payment-test
vers commit --tag "PayPal payment tests completed successfully"

# View commit history
vers log

Key Benefits Demonstrated

Time Savings

  • Traditional approach: Repeat “navigate → add to cart → checkout” for each payment method
  • Vers approach: Set up once, branch at decision points, test in parallel

State Preservation

  • Complex application states (logged in, cart filled) are captured and reusable
  • No need to rebuild test data for each scenario

Parallel Execution

  • Multiple test scenarios run simultaneously from identical starting points
  • Scales efficiently as test complexity grows

Resource Efficiency

  • Shared base states reduce redundant work
  • Only differences between scenarios require additional resources

Real-World Applications

This pattern works excellently for:
  • Payment method testing: Different checkout flows from the same cart state
  • User permission testing: Test admin vs. user flows from the same login point
  • Feature flag testing: Test different feature variations from identical setups
  • Browser compatibility: Run the same test sequence across different browser configurations
  • Data variation testing: Test different input combinations from the same form state

Common Issues and Solutions

Internet Access Problems

If you encounter DNS resolution errors or can’t install npm packages:
# Check current nameserver configuration
cat /etc/resolv.conf

# Test connectivity (if curl is available)
curl -s https://httpbin.org/ip

# Alternative test using nslookup (usually pre-installed)
nslookup google.com

# If DNS resolution fails, add nameserver manually
echo 'nameserver 8.8.8.8' > /etc/resolv.conf

# Alternative DNS servers you can use:
# echo 'nameserver 1.1.1.1' > /etc/resolv.conf     # Cloudflare
# echo 'nameserver 8.8.4.4' > /etc/resolv.conf     # Google alternative

# If curl isn't available and you need it:
# apt-get update && apt-get install -y curl

Docker Image Requirements

When creating custom Docker images for Vers, ensure you include:
  1. SSH server (openssh-server) with proper configuration
  2. Network tools (iproute2) for VM networking
  3. DNS configuration (nameserver in /etc/resolv.conf)
  4. Root SSH access enabled in SSH configuration
Missing any of these will prevent VM connectivity or internet access.

Summary

Vers transforms linear testing into an efficient, parallel workflow by:
  1. Eliminating repetitive setup through VM state branching
  2. Enabling parallel execution of related test scenarios
  3. Preserving complex application states for reuse across tests
  4. Scaling efficiently as test suites grow in complexity
The result is faster test execution, better resource utilization, and more maintainable test suites. Instead of thinking about tests as isolated scripts, Vers lets you think about test trees that branch from shared states.

Next Steps

  • Explore more complex branching scenarios with multiple decision points
  • Integrate Vers workflows into your CI/CD pipeline
  • Try the approach with other testing frameworks like Playwright or Selenium
  • Experiment with API testing scenarios using the same branching concepts