Writing Tests¶
Guidelines for creating effective tests for the SecAI SonarQube Plugin.
Backend Test Guidelines¶
JUnit 5 Test Structure¶
Follow the AAA (Arrange, Act, Assert) pattern as seen in IssueReporterTest:
@Test
void reportsSingleValidIssue() throws IOException, CentralSootUp.NoCentralSootUpInstanceException {
// Arrange
NewCCIssue ccIssue = mockValidCCIssue("MyClass", "void myMethod()", "src/MyClass.java",
"repo", "rule", Collections.emptyList(), Collections.emptyList(), new int[2][2]);
IssueReporter reporter = spy(new IssueReporter(context));
// Act
reporter.parseAndReportIssues(Map.of("1", ccIssue), "irrelevantPath");
// Assert
verify(context, times(1)).newIssue();
verify(mockIssue, times(1)).save();
}
Test Naming Conventions¶
Use descriptive test method names as shown in existing tests:
- reportsSingleValidIssue() - describes expected behavior
- skipsNullRuleKey() - describes edge case handling
- handlesEmptyIssueListGracefully() - describes error handling
Mocking with Mockito¶
Mock SonarQube dependencies extensively as shown in IssueReporterTest:
@BeforeEach
void setUp() {
context = mock(SensorContext.class);
fileSystem = mock(FileSystem.class);
predicates = mock(FilePredicates.class);
mockIssue = mock(NewIssue.class);
inputFile = mock(InputFile.class);
config = mock(Configuration.class);
when(context.fileSystem()).thenReturn(fileSystem);
when(context.newIssue()).thenReturn(mockIssue);
when(mockIssue.forRule(any(RuleKey.class))).thenReturn(mockIssue);
}
Configuration Mocking¶
Mock all SecAI configuration keys:
when(config.get(anyString())).thenAnswer(invocation -> {
String key = invocation.getArgument(0);
switch (key) {
case "sonar.secai.build.system":
return Optional.of("AUTO");
case "sonar.secai.ai.model":
return Optional.of("DefaultModel");
case "sonar.secai.ai.iterations":
return Optional.of("1");
case "sonar.projectKey":
return Optional.of("test_project");
case "sonar.secai.cognicrypt.messages":
return Optional.of("Shortened");
default:
return Optional.of("test_value");
}
});
Test Data Management¶
Use test resources for complex data as seen in project structure:
@Test
void shouldParseSarifReport() throws IOException {
String sarifContent = Files.readString(
Paths.get("src/test/resources/sarif/cognicrypt-testing-sarif-version.json"));
SarifReport report = parser.parse(sarifContent);
assertThat(report.getRuns()).hasSize(1);
}
Frontend Test Guidelines¶
React Component Testing¶
Use React Testing Library as shown in DetailedDescription.test.js:
import React from 'react';
import { render, screen } from '@testing-library/react';
import DetailedDescription from '../../main/js/sec_ai/components/Description/DetailedDescription';
describe('DetailedDescription', () => {
test('shows loading state when description is falsy', () => {
const { rerender } = render(<DetailedDescription description="" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
rerender(<DetailedDescription description={null} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});
Jest Configuration¶
The project uses custom Jest configuration in package.json:
"jest": {
"testEnvironment": "jest-environment-jsdom",
"setupFiles": ["<rootDir>/conf/jest/SetupTestEnvironment.js"],
"setupFilesAfterEnv": ["@testing-library/jest-dom/extend-expect"],
"coverageDirectory": "<rootDir>/target/coverage",
"coveragePathIgnorePatterns": [
"<rootDir>/node_modules",
"<rootDir>/tests"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules",
"<rootDir>/scripts",
"<rootDir>/conf"
]
}
Test Data and Fixtures¶
SARIF Test Files¶
The project includes specific SARIF test files in src/test/resources/sarif/:
cognicrypt-testing-sarif-version.json- Valid CogniCrypt outputempty-sarif.json- Empty SARIF structuremalformed-sarif.json- Invalid JSON syntaxmissing-field.json- Missing required SARIF fieldssame-lines.json- Multiple issues on same lineunknown-error-type.json- Unrecognized error types
Test Project Structure¶
The test_project contains:
src/test/resources/test_project/
├── pom.xml # Maven configuration
├── TestProject-1.0.jar # Pre-built JAR
├── expected-errors.json # Expected analysis results
└── src/main/java/com/example/
├── Main.java # Entry point
└── violations/ # Classes with violations
Mock Helper Methods¶
Create reusable mock helpers as shown in IssueReporterTest:
static NewCCIssue mockValidCCIssue(String className, String methodCall, String filePath,
String repo, String rule, List<String> preceding,
List<String> subsequent, int[][] location) {
NewCCIssue ccIssue = mock(NewCCIssue.class);
when(ccIssue.getClassName()).thenReturn(className);
when(ccIssue.getMethodCall()).thenReturn(methodCall);
when(ccIssue.getFilePath()).thenReturn(filePath);
when(ccIssue.getRuleKey()).thenReturn(RuleKey.of(repo, rule));
when(ccIssue.getErrorType()).thenReturn(CCErrorType.IMPRECISE_VALUE_EXTRACTION);
return ccIssue;
}
Coverage Requirements¶
Current Coverage Setup¶
- Backend: No coverage measurement (JaCoCo not configured)
- Frontend: Automatic coverage via Jest (reports in
target/coverage/) - Integration: SonarQube analysis combines available coverage data
Best Practices¶
Test Independence¶
- Each test should be independent as shown in existing tests
- Use
@BeforeEachfor setup as demonstrated inIssueReporterTest - Mock all external dependencies extensively
Error Handling Tests¶
Include defensive tests for edge cases:
@Test
void handlesAssembleIssueExceptionGracefully() {
doThrow(new RuntimeException("Assemble failed!"))
.when(ccIssue).assembleIssue(any(), any());
assertDoesNotThrow(() -> reporter.parseAndReportIssues(Map.of("1", ccIssue), "path"));
}
Maintainability¶
- Use descriptive test names that explain the scenario
- Group related tests in the same test class
- Use helper methods for common mock setups
- Follow the existing project patterns for consistency