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
| Framework | Technology | Test Type | Use Case |
|---|---|---|---|
| ABAP Unit | ABAP | Unit & Integration | Backend logic, classes, function modules |
| OPA5 | JavaScript (UI5) | Integration & E2E | UI5 application testing |
| UIVeri5 | JavaScript (Protractor) | End-to-End | Cross-app UI5 testing, CI/CD |
| PyTest | Python | Unit, Integration, API | Python 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
| Practice | Description |
|---|---|
| Use FOR TESTING | Mark test classes with FOR TESTING |
| Setup/Teardown | Use SETUP and TEARDOWN methods |
| Descriptive names | test_method_name_scenario |
| Test doubles | Mock dependencies to isolate unit |
| RISK LEVEL | Set appropriate risk level (HARMLESS, DANGEROUS, CRITICAL) |
| DURATION | Set 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
| Aspect | OPA5 | UIVeri5 |
|---|---|---|
| Execution | In-app (QUnit) | External (Browser automation) |
| Setup | Easy (part of UI5) | Complex (requires infrastructure) |
| Speed | Fast | Slower (real browser) |
| CI/CD | Possible but limited | Excellent (headless browsers) |
| Multi-app | Difficult | Easy |
| Use Case | Component/integration tests | End-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 -vPyTest 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
| Practice | Description |
|---|---|
| Use fixtures | @pytest.fixture for reusable test setup |
| Parametrize | @pytest.mark.parametrize for multiple inputs |
| Mock external deps | Use unittest.mock for databases/APIs |
| Descriptive names | test_method_scenario |
| Group tests | Use classes or modules to organize |
| Coverage | pytest-cov for code coverage reports |
Comparison Summary
| Aspect | ABAP Unit | OPA5 | UIVeri5 | PyTest |
|---|---|---|---|---|
| Learning Curve | Medium | Medium | High | Low |
| Setup Complexity | None (built-in) | Low | High | Low |
| Execution Speed | Fast | Fast | Slow | Fast |
| CI/CD Integration | Good (ATC) | Limited | Excellent | Excellent |
| Mocking Support | Good (Test Doubles) | Good (Mockserver) | Limited | Excellent |
| Best For | ABAP backend logic | UI5 app testing | E2E SAP scenarios | Python 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.
