I'm new to CI/CD and doing reports. This is for academic purposes. I need to generate reports on a Maven java project.
I have two test classes one for unit tests the other for integration tests. Each one is in a separate phase by itself. Both jobs are displayed as passed on GitLab Pipeline.
Here's the .gitlab-ci.yml for both tests:
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
stages:
- test
- verify
- sonarcloud
unit-tests:
stage: test
image: maven:3-openjdk-17
script:
- mvn test -Dtest=BookServiceTest # Executes only unit tests excluding integration tests
- ls -la target/
artifacts:
paths:
- target/surefire-reports
reports:
junit: target/surefire-reports/TEST-*.xml
integration-tests:
stage: test
image: maven:3-openjdk-17
script:
mvn verify -Dit.test=BookRepositoryIT # Executes only integration tests
artifacts:
paths:
- target/failsafe-reports
reports:
junit: target/failsafe-reports/TEST-*.xml
Both generate their respective reports (failsafe and surefire) and at the end of tests running I get this in the log:
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running BookServiceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.121 s - in BookServiceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco-maven-plugin:0.8.7:report (report) @ untitled ---
[INFO] Loading execution data file /builds/3il6143305/Indus/target/jacoco.exec
[INFO] Analyzed bundle 'untitled' with 3 classes
Which suggests to me that Jacoco is generating coverage data on the test classes.
Now finally in solarcloud-check job as part of the log I can see this generated verbose:
[INFO] Sensor JaCoCo XML Report Importer [jacoco]
[INFO] Importing 1 report(s). Turn your logs in debug mode in order to see the exhaustive list.
[INFO] Sensor JaCoCo XML Report Importer [jacoco] (done) | time=18ms
Which again tells me that GitLab (or SolarCloud) will successfully read from the jacoco.xml file.
But on the SolarCloud platform I get Quality Gate Failed. And it says I have 2 failing conditions on the new code:
On Maintainability I get : FAILED
B Maintainability Rating on New Code: Required A
And On Coverage I get
0.0% Coverage FAILED 0.0% Coverage: Required ≥ 80.0%
For Maintainability I can understand what I need to fix but for Coverage I really don't get why I'm getting 0.0% Coverage and it's the same for the whole project too.
Edit : I'll add the classes and Test classes as well for reproducibility purposes
public class Book {
private int id;
private String title;
private LocalDate releaseDate;
/**
* Constructs a new Book instance with the specified ID, title, and release date.
*
* @param id the unique identifier for the book
* @param title the title of the book
* @param releaseDate the release date of the book
*/
public Book(int id, String title, LocalDate releaseDate) {
this.id = id;
this.title = title;
this.releaseDate = releaseDate;
}
// Getters and setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public LocalDate getReleaseDate() { return releaseDate; }
public void setReleaseDate(LocalDate releaseDate) { this.releaseDate = releaseDate; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Book book = (Book) obj;
return id == book.id &&
Objects.equals(title, book.title) &&
Objects.equals(releaseDate, book.releaseDate);
}
@Override
public int hashCode() {
return Objects.hash(id, title, releaseDate);
}
}
package Repository;
import Entity.Book;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class BookRepository {
private final Connection connection;
public BookRepository() throws SQLException {
String url = System.getenv("DATABASE_URL");
String user = System.getenv("DATABASE_USER");
String password = System.getenv("DATABASE_PASSWORD");
this.connection = DriverManager.getConnection(url, user, password);
}
public void addBook(Book book) throws SQLException {
String sql = "INSERT INTO books (id, title, releaseDate) VALUES (?, ?, ?)";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, book.getId());
statement.setString(2, book.getTitle());
statement.setDate(3, java.sql.Date.valueOf(book.getReleaseDate()));
statement.executeUpdate();
}
}
public Book getBook(int id) throws SQLException {
String sql = "SELECT id, title, releaseDate FROM books WHERE id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, id);
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return new Book(
resultSet.getInt("id"),
resultSet.getString("title"),
resultSet.getDate("releaseDate").toLocalDate()
);
}
}
return null;
}
public void updateBook(Book book) throws SQLException {
String sql = "UPDATE books SET title = ?, releaseDate = ? WHERE id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, book.getTitle());
statement.setDate(2, java.sql.Date.valueOf(book.getReleaseDate()));
statement.setInt(3, book.getId());
statement.executeUpdate();
}
}
public void deleteBook(int id) throws SQLException {
String sql = "DELETE FROM books WHERE id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, id);
statement.executeUpdate();
}
}
public List<Book> getAllBooks() throws SQLException {
List<Book> books = new ArrayList<>();
String sql = "SELECT id, title, releaseDate FROM books";
try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
while (resultSet.next()) {
books.add(new Book(
resultSet.getInt("id"),
resultSet.getString("title"),
resultSet.getDate("releaseDate").toLocalDate()
));
}
}
return books;
}
public void createTable() throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE IF EXISTS books");
statement.execute("CREATE TABLE books (" +
"id INT PRIMARY KEY, " +
"title VARCHAR(255), " +
"releaseDate DATE)");
}
}
public void resetTable() throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.execute("DELETE FROM books");
}
}
}
package Service;
import Entity.Book;
import Repository.BookRepository;
import java.sql.SQLException;
import java.util.List;
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public void addBook(Book book) throws SQLException {
bookRepository.addBook(book);
}
public Book getBook(int id) throws SQLException {
return bookRepository.getBook(id);
}
public void updateBook(Book book) throws SQLException {
bookRepository.updateBook(book);
}
public void deleteBook(int id) throws SQLException {
bookRepository.deleteBook(id);
}
public List<Book> getAllBooks() throws SQLException {
return bookRepository.getAllBooks();
}
}
Unit Tests here :
import org.junit.Test;
import java.time.LocalDate;
public class BookServiceTest {
private void assertReleaseDateNotFuture(LocalDate releaseDate) {
LocalDate currentDate = LocalDate.now();
if (releaseDate.isAfter(currentDate)) {
throw new IllegalArgumentException("Release date should not be in the future.");
}
}
@Test(expected = IllegalArgumentException.class)
public void testReleaseDateNotInFuture() {
LocalDate futureDate = LocalDate.now().plusDays(1); // One day in the future
assertReleaseDateNotFuture(futureDate); // This should throw an exception
}
}
Integration Tests here :
import Entity.Book;
import Repository.BookRepository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.Assert;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;
public class BookRepositoryIT {
private BookRepository bookRepository;
@Before
public void setUp() throws SQLException {
bookRepository = new BookRepository();
bookRepository.createTable(); // Create the books table
}
@After
public void tearDown() throws SQLException {
bookRepository.resetTable(); // Reset the books table
}
@Test
public void testAddAndGetBook() throws SQLException {
LocalDate releaseDate = LocalDate.now();
Book newBook = new Book(1, "Test Book", releaseDate);
bookRepository.addBook(newBook);
Book retrievedBook = bookRepository.getBook(1);
Assert.assertNotNull(retrievedBook);
Assert.assertEquals("Test Book", retrievedBook.getTitle());
Assert.assertEquals(releaseDate, retrievedBook.getReleaseDate());
}
@Test
public void testUpdateBook() throws SQLException {
LocalDate releaseDate = LocalDate.now();
Book bookToUpdate = new Book(2, "Original Title", releaseDate);
bookRepository.addBook(bookToUpdate);
bookToUpdate.setTitle("Updated Title");
bookRepository.updateBook(bookToUpdate);
Book updatedBook = bookRepository.getBook(2);
Assert.assertNotNull(updatedBook);
Assert.assertEquals("Updated Title", updatedBook.getTitle());
}
@Test
public void testDeleteBook() throws SQLException {
LocalDate releaseDate = LocalDate.now();
Book bookToDelete = new Book(3, "Delete Title", releaseDate);
bookRepository.addBook(bookToDelete);
bookRepository.deleteBook(3);
Book deletedBook = bookRepository.getBook(3);
Assert.assertNull(deletedBook);
}
@Test
public void testGetAllBooks() throws SQLException {
LocalDate releaseDate = LocalDate.now();
bookRepository.addBook(new Book(4, "Book 1", releaseDate));
bookRepository.addBook(new Book(5, "Book 2", releaseDate));
List<Book> allBooks = bookRepository.getAllBooks();
Assert.assertTrue(allBooks.size() >= 2);
for (Book book : allBooks) {
Assert.assertTrue(book.getTitle().equals("Book 1") || book.getTitle().equals("Book 2"));
}
}
}
pom.xml is a bit lenghty but here you go:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>untitled</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version> <!-- Use the latest version available -->
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version> <!-- Use the latest version available -->
<configuration>
<configLocation>google_checks.xml</configLocation> <!-- Google's style guide -->
<consoleOutput>true</consoleOutput>
<outputFile>${project.build.directory}/checkstyle-result.xml</outputFile> <!-- Checkstyle results in XML format -->
</configuration>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version> <!-- Use the latest version available -->
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<!-- Specify patterns for integration test classes -->
<reportsDirectory>${project.build.directory}/failsafe-reports</reportsDirectory>
<includes>
<include>**/*IT.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version> <!-- Use the latest version available -->
<executions>
<execution>
<goals>
<goal>prepare-agent</goal> <!-- Prepares JaCoCo to measure coverage -->
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase> <!-- Bind report goal to a lifecycle phase -->
<goals>
<goal>report</goal> <!-- Creates code coverage report -->
</goals>
<configuration>
<outputDirectory>${project.build.directory}/site/jacoco</outputDirectory>
<dataFile>${project.build.directory}/jacoco.exec</dataFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sonar.organization>3il6143305</sonar.organization>
<sonar.java.checkstyle.reportPaths>${project.build.directory}/checkstyle-result.xml</sonar.java.checkstyle.reportPaths>
<sonar.coverage.jacoco.xmlReportPaths>${project.build.directory}/site/jacoco/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version> <!-- Use the latest version available -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.13.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.7.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.23</version>
</dependency>
</dependencies>
</project>
And finally I forgot to put the sonarcloud check scripts so here you go:
sonarcloud-check:
stage: sonarcloud
image: maven:3-openjdk-17
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- mvn clean verify sonar:sonar -Dsonar.projectKey=3il6143305_Indus -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
only:
- merge_requests
- master
- develop
- main