This guide explains how to write and run unit tests for the Jenkins shared library code located in libraries/tipipeline/.
The libraries/tipipeline/vars/*.groovy files contain utility functions used across many pipelines. Without tests:
- Regressions can silently break dozens of pipelines.
- Edge cases in branch name derivation, PR title parsing, etc. are easy to miss.
- Debugging failures in Jenkins replay is slow and painful.
Unit tests for pure functions (those with no Jenkins API dependency like sh, checkout, echo) can be run locally without a Jenkins server, giving rapid feedback.
- Java: OpenJDK 17+ (installed automatically by Homebrew)
- Groovy: 4.0+
Install on macOS:
brew install groovyVerify:
groovy --version
# Groovy Version: 5.0.5 JVM: 25.0.2 Vendor: Homebrew OS: Mac OS XTests live under libraries/tipipeline/tests/, mirroring the source layout:
libraries/tipipeline/
├── vars/ # Shared library functions (source under test)
│ ├── component.groovy
│ ├── cdc.groovy
│ ├── prow.groovy
│ └── ...
├── src/ # Optional: helper classes
└── test/ # Unit tests (you are here)
├── TestComponent.groovy
└── ... # More test files as needed
We use JUnit 4 with Groovy. JUnit 4 is bundled with the Groovy distribution, so no extra dependencies are needed.
Test classes use @RunWith(Enclosed.class) to group related test suites:
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.experimental.runners.Enclosed
import static org.junit.Assert.*
@RunWith(Enclosed.class)
class TestSomeFunction {
static class BehaviorGroupA {
// tests for one aspect
}
static class BehaviorGroupB {
// tests for another aspect
}
}Functions are loaded from the source file at runtime using GroovyShell. This avoids duplicating code:
static class ParseCIParams {
private def parseCIParams
@Before
void setUp() {
def script = new GroovyShell().parse(
new File("libraries/tipipeline/vars/component.groovy"))
parseCIParams = { String title ->
script.invokeMethod('parseCIParamsFromPRTitle', [title])
}
}
// ... tests
}Why this works: GroovyShell.parse() only compiles the code — it does not execute it. Functions that reference Jenkins APIs (like sh, checkout) are compiled but never called during the test, so they cause no errors. Only the function under test (a pure function) is invoked.
We use table-driven tests to reduce boilerplate. Each @Test method defines a table of cases and iterates over them:
@Test
void shouldExtractParamsFromTitle() {
def cases = [
[title: 'feat: support fast read | tidb=pr/123',
expected: [tidb: 'pr/123']],
[title: 'feat: support fast read | tidb=pr/123 pd=@v8.5.0',
expected: [tidb: 'pr/123', pd: '@v8.5.0']],
// ... more cases
]
cases.each { c ->
def result = parseCIParams(c.title)
assertEquals("case: ${c.title}", c.expected, result)
}
}Benefits:
- Adding a new test case is a one-line addition to the table.
- The assertion message includes the case description, making failures easy to diagnose.
- No need for repetitive
@Testmethods.
Name test methods in should style to describe the expected behavior:
@Test
void shouldExtractSingleParamAfterPipe() { ... }
@Test
void shouldIgnoreParamOnCherryPickTitle() { ... }
@Test
void shouldDeriveBranchFromTarget() { ... }Always include a descriptive message as the first argument to assertEquals:
assertEquals("${component} on ${target}: ${title}", expected, result)This makes test failures instantly readable.
Run a single test file with the groovy command:
groovy libraries/tipipeline/tests/TestComponent.groovyGroovy automatically detects JUnit 4 annotations and runs the tests. Example output:
computeBranchFromPR component: tidb, prTargetBranch: master, prTitle: feat: support fast read | tidb=pr/123, trunkBranch: master
JUnit 4 Runner, Tests: 6, Failures: 0, Time: 925
Exit code is 0 on success and 1 on failure, so it integrates with CI scripts:
groovy libraries/tipipeline/tests/TestComponent.groovy || {
echo "Tests failed!"
exit 1
}- Test pure functions only. Functions that call
sh,checkout,withCredentials, etc. cannot run outside Jenkins. Keep these dependencies minimal and extract pure logic into testable helper functions. - Use table-driven style. It minimizes boilerplate and makes adding new cases trivial.
- Cover edge cases. Empty input, special characters, boundary values, cherry-pick patterns, etc.
- Name tests as specifications.
shouldReturnEmptyWhenNoPipe()is better thantestPipe(), and much better thantest1(). - Include assertion messages. They save debugging time when a case fails.
- Keep tests fast. Each test file should complete in under 5 seconds. If a test is slow, it won't be run frequently.
- Don't test Jenkins internals. Focus on your logic, not on whether
shworks. - Don't duplicate the function under test. Always load it from the source file with
GroovyShell. - Don't write one method per case. Use tables instead.
- Don't skip error messages in assertions. A bare
assertEquals(expected, actual)gives no context on failure.
See libraries/tipipeline/tests/TestComponent.groovy for a complete example testing:
parseCIParamsFromPRTitle— extracting CI parameters from PR titlescomputeBranchFromPR— deriving component branches from target branches and PR titles
Both functions are pure (no Jenkins API calls) and the tests cover 27+ cases across 6 @Test methods using table-driven style.
- Create
libraries/tipipeline/tests/Test<YourFunction>.groovy - Import JUnit 4 classes and
@RunWith(Enclosed.class)for nested suites - Load the source function via
GroovyShellin@Before - Write table-driven
@Testmethods - Run with
groovy libraries/tipipeline/tests/Test<YourFunction>.groovy