Testing Strategy
Sakai uses multiple testing approaches:
Unit Tests : Test individual classes and methods (JUnit, Mockito)
Integration Tests : Test service interactions (Spring Test)
Web Component Tests : Test frontend components (Web Test Runner, Chai)
E2E Tests : Test complete user flows (Playwright)
Java Unit Tests
JUnit 5 Tests
Write unit tests using JUnit 5:
package org.sakaiproject.mytool.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions. * ;
import java.util.List;
@ DisplayName ( "MyToolService Tests" )
class MyToolServiceTest {
private MyToolService service ;
@ BeforeEach
void setUp () {
service = new MyToolServiceImpl ();
}
@ Test
@ DisplayName ( "Should return data for valid site ID" )
void testGetDataValidSite () {
List < String > data = service . getData ( "site-123" );
assertNotNull (data);
assertFalse ( data . isEmpty ());
}
@ Test
@ DisplayName ( "Should throw exception for null site ID" )
void testGetDataNullSite () {
assertThrows (
IllegalArgumentException . class ,
() -> service . getData ( null )
);
}
}
Mockito for Mocking
Use Mockito to mock dependencies:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito. * ;
import static org.junit.jupiter.api.Assertions. * ;
@ ExtendWith ( MockitoExtension . class )
class MyToolServiceImplTest {
@ Mock
private UserDirectoryService userService ;
@ Mock
private SecurityService securityService ;
@ InjectMocks
private MyToolServiceImpl service ;
@ Test
void testGetUserData () {
// Arrange
User mockUser = mock ( User . class );
when ( mockUser . getId ()). thenReturn ( "user-123" );
when ( userService . getCurrentUser ()). thenReturn (mockUser);
// Act
String userId = service . getCurrentUserId ();
// Assert
assertEquals ( "user-123" , userId);
verify (userService). getCurrentUser ();
}
}
Spring Integration Tests
Test Configuration
Create test configuration class:
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
@ TestConfiguration
public class MyToolTestConfiguration {
@ Bean
@ Primary
public MyToolService myToolService () {
return new MyToolServiceImpl ();
}
}
Integration Test
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import static org.junit.jupiter.api.Assertions. * ;
@ SpringBootTest
@ ContextConfiguration ( classes = MyToolTestConfiguration . class )
class MyToolServiceIntegrationTest {
@ Autowired
private MyToolService service ;
@ Test
void testServiceIntegration () {
List < String > data = service . getData ( "site-123" );
assertNotNull (data);
}
}
Web Component Tests
Basic Component Test
Create tests using Web Test Runner and Chai:
import "../sakai-my-component.js" ;
import { expect , fixture , html , waitUntil } from "@open-wc/testing" ;
describe ( "sakai-my-component tests" , () => {
it ( "renders correctly" , async () => {
const el = await fixture ( html `
<sakai-my-component site-id="test-site"></sakai-my-component>
` );
expect ( el ). to . exist ;
expect ( el . siteId ). to . equal ( "test-site" );
});
it ( "is accessible" , async () => {
const el = await fixture ( html `
<sakai-my-component></sakai-my-component>
` );
await expect ( el ). to . be . accessible ();
});
});
Mock Network Calls
Use FetchMock to mock API calls:
import "../sakai-tasks.js" ;
import { expect , fixture , html , waitUntil } from "@open-wc/testing" ;
import fetchMock from "fetch-mock" ;
describe ( "sakai-tasks tests" , () => {
afterEach (() => fetchMock . restore ());
it ( "loads tasks from API" , async () => {
const mockTasks = [
{ id: 1 , title: "Task 1" , completed: false },
{ id: 2 , title: "Task 2" , completed: true },
];
fetchMock . get ( "/api/tasks?siteId=site-123" , mockTasks , {
overwriteRoutes: true ,
});
const el = await fixture ( html `
<sakai-tasks site-id="site-123"></sakai-tasks>
` );
await waitUntil (() => el . _tasks . length > 0 );
expect ( el . _tasks ). to . have . lengthOf ( 2 );
expect ( el . _tasks [ 0 ]. title ). to . equal ( "Task 1" );
});
it ( "handles API errors" , async () => {
fetchMock . get ( "/api/tasks?siteId=site-123" , 500 , {
overwriteRoutes: true ,
});
const el = await fixture ( html `
<sakai-tasks site-id="site-123"></sakai-tasks>
` );
await waitUntil (() => el . _error !== null );
expect ( el . _error ). to . exist ;
});
});
Mock Browser APIs
Use Sinon to mock browser APIs:
import { expect , fixture , html } from "@open-wc/testing" ;
import sinon from "sinon" ;
describe ( "sakai-notifications tests" , () => {
it ( "requests notification permission" , async () => {
const stub = sinon . stub ( Notification , "requestPermission" )
. resolves ( "granted" );
const el = await fixture ( html `
<sakai-notifications></sakai-notifications>
` );
await el . requestPermission ();
expect ( stub . calledOnce ). to . be . true ;
expect ( el . permission ). to . equal ( "granted" );
stub . restore ();
});
});
Test User Interactions
it ( "handles button click" , async () => {
const el = await fixture ( html `
<sakai-my-component></sakai-my-component>
` );
const button = el . shadowRoot . querySelector ( ".save-button" );
expect ( button ). to . exist ;
button . click ();
await waitUntil (() => el . _saved === true );
expect ( el . _saved ). to . be . true ;
});
it ( "handles input change" , async () => {
const el = await fixture ( html `
<sakai-my-component></sakai-my-component>
` );
const input = el . shadowRoot . querySelector ( "input" );
input . value = "test value" ;
input . dispatchEvent ( new Event ( "input" ));
await el . updateComplete ;
expect ( el . inputValue ). to . equal ( "test value" );
});
Playwright E2E Tests
Test Structure
Playwright tests are in e2e-tests/src/test/java/org/sakaiproject/e2e/tests/:
package org.sakaiproject.e2e.tests;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import com.microsoft.playwright.Locator;
import org.junit.jupiter.api. * ;
import org.sakaiproject.e2e.support.SakaiUiTestBase;
import java.util.List;
@ TestMethodOrder ( MethodOrderer . OrderAnnotation . class )
class MyToolTest extends SakaiUiTestBase {
private static String sakaiUrl ;
@ Test
@ Order ( 1 )
void canCreateCourse () {
sakai . login ( "instructor1" );
sakaiUrl = sakai . createCourse (
"instructor1" ,
List . of ( "sakai \. mytool" )
);
}
@ Test
@ Order ( 2 )
void canUseTool () {
sakai . login ( "instructor1" );
sakai . gotoPath (sakaiUrl);
sakai . toolClick ( "My Tool" );
// Test interactions
page . locator ( "#title" ). fill ( "Test Title" );
page . locator ( "#save-button" ). click ();
// Assert results
assertThat ( page . locator ( ".success-message" ))
. isVisible ();
}
}
Helper Methods
Use SakaiHelper methods for common operations:
// Login
sakai . login ( "username" );
// Navigate to site
sakai . gotoPath (sakaiUrl);
// Click tool in navigation
sakai . toolClick ( "Tool Name" );
// Create course with tools
String siteUrl = sakai . createCourse (
"instructor1" ,
List . of ( "sakai \. announcements" , "sakai \. schedule" )
);
// Select date
sakai . selectDate ( "#datepicker" , "06/01/2025 08:30 am" );
Run E2E Tests
All Tests
Single Test Class
Single Test Method
Headed Mode (Watch Browser)
export JAVA_HOME = $( /usr/libexec/java_home -v 17 )
export PLAYWRIGHT_BASE_URL = https :// localhost : 8080
mvn -f e2e-tests/pom.xml test
Test Artifacts
Playwright saves artifacts to e2e-tests/target/playwright-artifacts/:
Traces : Full execution traces for debugging
Videos : Screen recordings of test runs
Screenshots : Final screenshots on failure
Running Tests
Java Unit Tests
All Tests in Module
Single Test Class
Single Test Method
Skip Tests
Web Component Tests
All Components
Single Component
Watch Mode
Specific Browser
cd webcomponents/tool/src/main/frontend
npm run test
Code Coverage
Web component testing intentionally omits coverage metrics. Focus on testing common user pathways and edge cases rather than chasing coverage percentages.
Java Coverage with JaCoCo
Generate coverage reports:
mvn clean test jacoco:report
View report at target/site/jacoco/index.html.
Best Practices
Use descriptive test names that explain what is being tested
Use @DisplayName in Java for readable test output
Follow pattern: should[ExpectedBehavior]When[StateUnderTest]
Each test should be independent and isolated
Don’t rely on test execution order (except E2E tests)
Clean up resources in @AfterEach or afterEach()
Use fresh fixtures for each test
Use realistic test data
Avoid hard-coded magic values
Create test data factories or builders
Use FetchMock for consistent API responses
Use descriptive assertion messages
Assert one thing per test when possible
Test both positive and negative cases
Include accessibility checks for components
When changing user-visible UI flows (navigation, forms, submissions, dialogs), add or update a Playwright test in e2e-tests/src/test/java/org/sakaiproject/e2e/tests. If a Playwright test is not practical, document why in the PR description.
Test-Driven Development (TDD)
TDD is recommended for component development:
Write failing test
Write a test that exercises your yet-to-be-written code: it ( "should display user name" , async () => {
const el = await fixture ( html `
<sakai-user-display user-id="user-123"></sakai-user-display>
` );
await waitUntil (() => el . userName );
expect ( el . userName ). to . equal ( "John Doe" );
});
Run test (it should fail)
Write minimal code to pass
Implement just enough code to make the test pass.
Refactor
Clean up code while keeping tests green.
Repeat
Continue the cycle for each new feature.
Continuous Integration
Tests run automatically on:
Pull Requests : All tests must pass before merge
Nightly Builds : Full test suite on nightly servers
Pre-merge : Checkstyle and compilation checks
Next Steps
Contributing Submit your tested code to Sakai
Web Components Learn more about component development