Skip to main content

Automated Testing Strategy: ABAP Unit, OPA5/UIVeri5, and PyTest Comparison

In modern SAP development, automated testing is essential for maintaining quality, enabling continuous integration, and supporting agile development. This guide compares the major testing frameworks across the SAP technology stack: ABAP Unit, OPA5, UIVeri5, and PyTest.

Testing Framework Overview

FrameworkTechnologyTest TypeUse Case
ABAP UnitABAPUnit & IntegrationBackend logic, classes, function modules
OPA5JavaScript (UI5)Integration & E2EUI5 application testing
UIVeri5JavaScript (Protractor)End-to-EndCross-app UI5 testing, CI/CD
PyTestPythonUnit, Integration, APIPython services, API testing, data science

1. ABAP Unit Testing

What is ABAP Unit?

ABAP Unit is the built-in testing framework for ABAP, integrated into SAP NetWeaver. It supports unit testing and integration testing of ABAP classes, methods, and function modules.

Basic ABAP Unit Test

CLASS zcl_calculator DEFINITION PUBLIC.
  PUBLIC SECTION.
    METHODS add IMPORTING iv_a TYPE i
                          iv_b TYPE i
                RETURNING VALUE(rv_result) TYPE i.
    
    METHODS divide IMPORTING iv_a TYPE i
                             iv_b TYPE i
                   RETURNING VALUE(rv_result) TYPE p
                   RAISING zcx_division_by_zero.
ENDCLASS.

CLASS zcl_calculator IMPLEMENTATION.
  METHOD add.
    rv_result = iv_a + iv_b.
  ENDMETHOD.
  
  METHOD divide.
    IF iv_b = 0.
      RAISE EXCEPTION TYPE zcx_division_by_zero.
    ENDIF.
    rv_result = iv_a / iv_b.
  ENDMETHOD.
ENDCLASS.

" ============================================
" TEST CLASS
" ============================================
CLASS ltc_calculator DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    DATA mo_calculator TYPE REF TO zcl_calculator.
    
    METHODS setup.
    METHODS test_add FOR TESTING.
    METHODS test_divide FOR TESTING.
    METHODS test_divide_by_zero FOR TESTING.
ENDCLASS.

CLASS ltc_calculator IMPLEMENTATION.
  METHOD setup.
    " Setup runs before each test
    CREATE OBJECT mo_calculator.
  ENDMETHOD.
  
  METHOD test_add.
    " Arrange
    DATA(lv_result) = mo_calculator->add(
      iv_a = 5
      iv_b = 3
    ).
    
    " Assert
    cl_abap_unit_assert=>assert_equals(
      act = lv_result
      exp = 8
      msg = 'Addition should work correctly'
    ).
  ENDMETHOD.
  
  METHOD test_divide.
    DATA(lv_result) = mo_calculator->divide(
      iv_a = 10
      iv_b = 2
    ).
    
    cl_abap_unit_assert=>assert_equals(
      act = lv_result
      exp = 5
      msg = 'Division should work correctly'
    ).
  ENDMETHOD.
  
  METHOD test_divide_by_zero.
    TRY.
        mo_calculator->divide(
          iv_a = 10
          iv_b = 0
        ).
        
        " Should not reach here
        cl_abap_unit_assert=>fail(
          msg = 'Should have raised exception'
        ).
        
      CATCH zcx_division_by_zero.
        " Expected exception - test passes
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

Test Doubles (Mocking in ABAP)

" Class under test depends on database
CLASS zcl_customer_service DEFINITION.
  PUBLIC SECTION.
    INTERFACES zif_customer_repository.
    
    METHODS get_customer_name
      IMPORTING iv_kunnr TYPE kunnr
      RETURNING VALUE(rv_name) TYPE string.
    
  PRIVATE SECTION.
    DATA mo_repository TYPE REF TO zif_customer_repository.
ENDCLASS.

CLASS zcl_customer_service IMPLEMENTATION.
  METHOD get_customer_name.
    DATA(ls_customer) = mo_repository->get_by_id( iv_kunnr ).
    rv_name = ls_customer-name1.
  ENDMETHOD.
ENDCLASS.

" ============================================
" TEST CLASS WITH MOCK
" ============================================
CLASS ltc_customer_service DEFINITION FOR TESTING.
  PRIVATE SECTION.
    DATA mo_service TYPE REF TO zcl_customer_service.
    DATA mo_mock_repo TYPE REF TO zif_customer_repository.
    
    METHODS setup.
    METHODS test_get_customer_name FOR TESTING.
ENDCLASS.

CLASS ltc_customer_service IMPLEMENTATION.
  METHOD setup.
    " Create mock repository
    mo_mock_repo = CAST zif_customer_repository(
      cl_abap_testdouble=>create( 'ZIF_CUSTOMER_REPOSITORY' )
    ).
    
    " Configure mock behavior
    cl_abap_testdouble=>configure_call( mo_mock_repo )->ignore_all_parameters( ).
    
    mo_mock_repo->get_by_id( '0001000001' ).
    
    cl_abap_testdouble=>configure_call( mo_mock_repo )->and_expect( )->is_called_once( ).
    cl_abap_testdouble=>configure_call( mo_mock_repo )->returning(
      VALUE kna1( kunnr = '0001000001' name1 = 'ACME Corporation' )
    ).
    
    " Inject mock into service
    CREATE OBJECT mo_service.
    mo_service->zif_customer_repository~mo_repository = mo_mock_repo.
  ENDMETHOD.
  
  METHOD test_get_customer_name.
    DATA(lv_name) = mo_service->get_customer_name( '0001000001' ).
    
    cl_abap_unit_assert=>assert_equals(
      act = lv_name
      exp = 'ACME Corporation'
      msg = 'Should return mocked customer name'
    ).
    
    " Verify mock was called
    cl_abap_testdouble=>verify_expectations( mo_mock_repo ).
  ENDMETHOD.
ENDCLASS.

ABAP Unit Best Practices

PracticeDescription
Use FOR TESTINGMark test classes with FOR TESTING
Setup/TeardownUse SETUP and TEARDOWN methods
Descriptive namestest_method_name_scenario
Test doublesMock dependencies to isolate unit
RISK LEVELSet appropriate risk level (HARMLESS, DANGEROUS, CRITICAL)
DURATIONSet expected duration (SHORT, MEDIUM, LONG)

2. OPA5 (One Page Acceptance Tests)

What is OPA5?

OPA5 is a JavaScript testing framework for SAP UI5 applications. It enables integration and end-to-end testing from the user's perspective, testing UI interactions and data flow.

OPA5 Test Structure

// test/integration/pages/BookList.js
sap.ui.define([
  "sap/ui/test/Opa5",
  "sap/ui/test/actions/Press",
  "sap/ui/test/matchers/AggregationLengthEquals",
  "sap/ui/test/matchers/Properties"
], function(Opa5, Press, AggregationLengthEquals, Properties) {
  "use strict";

  Opa5.createPageObjects({
    onTheBookListPage: {
      
      actions: {
        iPressOnTheFirstBook: function() {
          return this.waitFor({
            id: "bookList",
            viewName: "BookList",
            actions: new Press(),
            errorMessage: "The book list was not found"
          });
        },
        
        iSearchForBook: function(sSearchTerm) {
          return this.waitFor({
            id: "searchField",
            viewName: "BookList",
            actions: function(oSearchField) {
              oSearchField.setValue(sSearchTerm);
            },
            errorMessage: "Search field not found"
          });
        }
      },
      
      assertions: {
        iShouldSeeTheBookList: function() {
          return this.waitFor({
            id: "bookList",
            viewName: "BookList",
            success: function() {
              Opa5.assert.ok(true, "The book list is displayed");
            },
            errorMessage: "The book list was not found"
          });
        },
        
        theBookListShouldHaveEntries: function(iExpectedCount) {
          return this.waitFor({
            id: "bookList",
            viewName: "BookList",
            matchers: new AggregationLengthEquals({
              name: "items",
              length: iExpectedCount
            }),
            success: function() {
              Opa5.assert.ok(true, `Book list has ${iExpectedCount} entries`);
            },
            errorMessage: "Book list doesn't have expected count"
          });
        },
        
        iShouldSeeBookWithTitle: function(sTitle) {
          return this.waitFor({
            id: "bookList",
            viewName: "BookList",
            matchers: new Properties({
              title: sTitle
            }),
            success: function() {
              Opa5.assert.ok(true, `Book with title "${sTitle}" found`);
            },
            errorMessage: `Book with title "${sTitle}" not found`
          });
        }
      }
    }
  });
});

OPA5 Journey (Test Scenario)

// test/integration/BookListJourney.js
sap.ui.define([
  "sap/ui/test/opaQunit",
  "./pages/BookList",
  "./pages/BookDetail"
], function(opaTest) {
  "use strict";

  QUnit.module("Book List");

  opaTest("Should see the book list", function(Given, When, Then) {
    // Arrange
    Given.iStartMyApp();
    
    // Act - user navigates to app
    
    // Assert
    Then.onTheBookListPage.iShouldSeeTheBookList();
    Then.onTheBookListPage.theBookListShouldHaveEntries(5);
  });

  opaTest("Should navigate to book detail", function(Given, When, Then) {
    // Act
    When.onTheBookListPage.iPressOnTheFirstBook();
    
    // Assert
    Then.onTheBookDetailPage.iShouldSeeTheBookDetails();
    Then.onTheBookDetailPage.theBookTitleShouldBe("1984");
  });

  opaTest("Should search for a book", function(Given, When, Then) {
    // Arrange
    Given.iStartMyApp();
    
    // Act
    When.onTheBookListPage.iSearchForBook("Clean Code");
    
    // Assert
    Then.onTheBookListPage.theBookListShouldHaveEntries(1);
    Then.onTheBookListPage.iShouldSeeBookWithTitle("Clean Code");
    
    // Cleanup
    Then.iTeardownMyApp();
  });
});

OPA5 Best Practices

  • ✅ Use Page Objects pattern for reusability
  • ✅ Wait for elements before interacting (waitFor)
  • ✅ Test user journeys, not implementation details
  • ✅ Use descriptive test names
  • ✅ Clean up after tests (iTeardownMyApp)
  • ✅ Mock backend with mockserver for consistent tests

3. UIVeri5

What is UIVeri5?

UIVeri5 is an end-to-end testing framework for UI5 apps built on Protractor. It's ideal for CI/CD pipelines and cross-application testing scenarios.

UIVeri5 Test Example

// test/e2e/bookList.spec.js
describe("Book List", function() {
  
  it("should display the book list", function() {
    // Navigate to app
    browser.get("http://localhost:8080/index.html");
    
    // Wait for list to load
    const bookList = element(by.id("bookList"));
    expect(bookList.isPresent()).toBeTruthy();
    
    // Check list has items
    const items = element.all(by.control({
      viewName: "BookList",
      controlType: "sap.m.StandardListItem"
    }));
    
    expect(items.count()).toBeGreaterThan(0);
  });
  
  it("should navigate to book detail on click", function() {
    // Click first book
    const firstBook = element(by.control({
      viewName: "BookList",
      controlType: "sap.m.StandardListItem",
      bindingPath: {
        path: "/Books/0"
      }
    }));
    
    firstBook.click();
    
    // Wait for detail page
    const detailPage = element(by.control({
      viewName: "BookDetail",
      controlType: "sap.m.Page"
    }));
    
    expect(detailPage.isPresent()).toBeTruthy();
    
    // Check title
    const titleElement = element(by.control({
      viewName: "BookDetail",
      controlType: "sap.m.ObjectHeader",
      properties: { title: "1984" }
    }));
    
    expect(titleElement.isPresent()).toBeTruthy();
  });
  
  it("should place an order", function() {
    // Navigate to book detail
    browser.get("http://localhost:8080/index.html#/book/1");
    
    // Enter quantity
    const quantityInput = element(by.id("quantityInput"));
    quantityInput.clear();
    quantityInput.sendKeys("2");
    
    // Click order button
    const orderButton = element(by.control({
      viewName: "BookDetail",
      controlType: "sap.m.Button",
      properties: { text: "Order Book" }
    }));
    
    orderButton.click();
    
    // Wait for success message
    const messageBox = element(by.control({
      controlType: "sap.m.MessageBox"
    }));
    
    expect(messageBox.isPresent()).toBeTruthy();
  });
});

UIVeri5 Configuration

// conf.js
exports.config = {
  profile: {
    name: "integration"
  },
  
  baseUrl: "http://localhost:8080",
  
  specs: ["test/e2e/**/*.spec.js"],
  
  browsers: [{
    browserName: "chrome",
    capabilities: {
      chromeOptions: {
        args: ["--headless", "--disable-gpu", "--no-sandbox"]
      }
    }
  }],
  
  reporters: [{
    name: "./reporter/junitReporter",
    reportName: "junit-report"
  }, {
    name: "./reporter/screenshotReporter",
    takeScreenshotsOnActions: true,
    takeScreenshotsOnFailure: true
  }],
  
  timeouts: {
    getPageTimeout: 10000,
    allScriptsTimeout: 30000,
    defaultTimeoutInterval: 60000
  }
};

UIVeri5 vs OPA5

AspectOPA5UIVeri5
ExecutionIn-app (QUnit)External (Browser automation)
SetupEasy (part of UI5)Complex (requires infrastructure)
SpeedFastSlower (real browser)
CI/CDPossible but limitedExcellent (headless browsers)
Multi-appDifficultEasy
Use CaseComponent/integration testsEnd-to-end system tests

4. PyTest

What is PyTest?

PyTest is Python's most popular testing framework. It's used for testing Python services, API integrations, data science pipelines, and SAP HANA connections.

PyTest Basic Example

# calculator.py
class Calculator:
    def add(self, a: int, b: int) -> int:
        return a + b
    
    def divide(self, a: int, b: int) -> float:
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b

# test_calculator.py
import pytest
from calculator import Calculator

@pytest.fixture
def calculator():
    """Fixture to create calculator instance"""
    return Calculator()

def test_add(calculator):
    """Test addition"""
    result = calculator.add(5, 3)
    assert result == 8

def test_divide(calculator):
    """Test division"""
    result = calculator.divide(10, 2)
    assert result == 5.0

def test_divide_by_zero(calculator):
    """Test division by zero raises exception"""
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        calculator.divide(10, 0)

# Run tests:
# pytest test_calculator.py -v

PyTest for SAP HANA Integration

# customer_service.py
from hdbcli import dbapi

class CustomerService:
    def __init__(self, connection):
        self.connection = connection
    
    def get_customer(self, customer_id: str):
        cursor = self.connection.cursor()
        cursor.execute(
            "SELECT CustomerId, Name, Revenue FROM Customers WHERE CustomerId = ?",
            (customer_id,)
        )
        result = cursor.fetchone()
        cursor.close()
        
        if not result:
            raise ValueError(f"Customer {customer_id} not found")
        
        return {
            'id': result[0],
            'name': result[1],
            'revenue': float(result[2])
        }

# test_customer_service.py
import pytest
from unittest.mock import Mock, MagicMock
from customer_service import CustomerService

@pytest.fixture
def mock_connection():
    """Mock HANA connection"""
    connection = Mock()
    cursor = MagicMock()
    connection.cursor.return_value = cursor
    return connection, cursor

def test_get_customer_success(mock_connection):
    """Test successful customer retrieval"""
    connection, cursor = mock_connection
    
    # Configure mock to return customer data
    cursor.fetchone.return_value = ('C001', 'ACME Corp', 150000.00)
    
    service = CustomerService(connection)
    customer = service.get_customer('C001')
    
    assert customer['id'] == 'C001'
    assert customer['name'] == 'ACME Corp'
    assert customer['revenue'] == 150000.00
    
    # Verify SQL was called
    cursor.execute.assert_called_once()

def test_get_customer_not_found(mock_connection):
    """Test customer not found raises exception"""
    connection, cursor = mock_connection
    cursor.fetchone.return_value = None
    
    service = CustomerService(connection)
    
    with pytest.raises(ValueError, match="Customer C999 not found"):
        service.get_customer('C999')

PyTest Parametrized Tests

# test_validation.py
import pytest

def validate_email(email: str) -> bool:
    """Simple email validation"""
    return '@' in email and '.' in email.split('@')[1]

@pytest.mark.parametrize("email,expected", [
    ("user@example.com", True),
    ("user@company.co.uk", True),
    ("invalid.email", False),
    ("@example.com", False),
    ("user@", False),
    ("", False)
])
def test_email_validation(email, expected):
    """Test email validation with multiple inputs"""
    assert validate_email(email) == expected

# Runs 6 separate tests automatically!

PyTest Best Practices

PracticeDescription
Use fixtures@pytest.fixture for reusable test setup
Parametrize@pytest.mark.parametrize for multiple inputs
Mock external depsUse unittest.mock for databases/APIs
Descriptive namestest_method_scenario
Group testsUse classes or modules to organize
Coveragepytest-cov for code coverage reports

Comparison Summary

AspectABAP UnitOPA5UIVeri5PyTest
Learning CurveMediumMediumHighLow
Setup ComplexityNone (built-in)LowHighLow
Execution SpeedFastFastSlowFast
CI/CD IntegrationGood (ATC)LimitedExcellentExcellent
Mocking SupportGood (Test Doubles)Good (Mockserver)LimitedExcellent
Best ForABAP backend logicUI5 app testingE2E SAP scenariosPython services

Test Pyramid Strategy

                        /\
                       /  \  E2E Tests (UIVeri5)
                      /    \  - Few, expensive
                     /------\  - Full system tests
                    /        \
                   / Integr-  \ Integration Tests (OPA5, ABAP Unit)
                  /   ation    \ - More than unit, fewer than E2E
                 /    Tests     \ - Test component interactions
                /--------------\
               /                \
              /   Unit Tests     \ Unit Tests (ABAP Unit, PyTest)
             /  (ABAP, Python)    \ - Many, fast, cheap
            /______________________\ - Test individual functions/classes

Recommended Distribution

  • 70% Unit Tests – Fast, isolated, catch most bugs
  • 20% Integration Tests – Test component interactions
  • 10% E2E Tests – Critical user journeys only

CI/CD Integration Example

Jenkins Pipeline

// Jenkinsfile
pipeline {
  agent any
  
  stages {
    stage('ABAP Unit Tests') {
      steps {
        script {
          // Run ABAP Unit via ATC
          abapGit.runAbapUnit(
            host: 'sap-system.com',
            package: 'Z_MY_PACKAGE'
          )
        }
      }
    }
    
    stage('Python Unit Tests') {
      steps {
        sh '''
          python -m pytest tests/ -v --cov=src --cov-report=xml
        '''
      }
    }
    
    stage('UI5 Integration Tests') {
      steps {
        sh '''
          npm install
          npm run test:opa5
        '''
      }
    }
    
    stage('E2E Tests') {
      steps {
        sh '''
          npm run test:uiveri5
        '''
      }
    }
  }
  
  post {
    always {
      junit 'test-results/**/*.xml'
      publishHTML([
        reportDir: 'coverage',
        reportFiles: 'index.html',
        reportName: 'Coverage Report'
      ])
    }
  }
}

Best Practices Across All Frameworks

✅ DO

  • Write tests alongside code (not after)
  • Follow AAA pattern (Arrange, Act, Assert)
  • Keep tests independent (no shared state)
  • Use descriptive test names
  • Mock external dependencies
  • Run tests in CI/CD pipeline
  • Maintain tests like production code
  • Aim for 80%+ code coverage

❌ AVOID

  • Testing implementation details
  • Skipping tests to "save time"
  • Overly complex test setup
  • Ignoring failing tests
  • Testing framework code (trust the framework)
  • Long-running E2E tests for every change

Conclusion

A comprehensive testing strategy uses multiple frameworks working together:

  • ABAP Unit for backend business logic
  • PyTest for Python services and APIs
  • OPA5 for UI5 component testing
  • UIVeri5 for critical E2E journeys

Test automation isn't optional — it's the foundation of modern SAP development.

About the Author: Yogesh Pandey is a passionate developer and consultant specializing in SAP technologies and full-stack development.