Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Improve metrics integration test coverage #9306

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9dfef93
+Enabled shouldReturnAverageScores() test
raffifasaro Sep 5, 2024
520571d
+shouldReturnCategories() test
raffifasaro Sep 10, 2024
78c0daa
Merge remote-tracking branch 'origin/develop' into chore/learning-ana…
raffifasaro Sep 11, 2024
5ab6ddf
+ shouldReturnTeamId() test
raffifasaro Sep 11, 2024
487e291
Merge remote-tracking branch 'origin/develop' into chore/learning-ana…
raffifasaro Sep 12, 2024
1c48a08
Merge fix
raffifasaro Sep 12, 2024
dfb6b15
Score test fix
raffifasaro Sep 12, 2024
88eb745
Some fixes
raffifasaro Sep 13, 2024
7c46d4f
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 13, 2024
387a22b
fix Score test
raffifasaro Sep 14, 2024
e31e5fb
fix Completed Test
raffifasaro Sep 14, 2024
31d97f1
fix AverageScore Test
raffifasaro Sep 14, 2024
8d140de
remove CourseUtilService changes
raffifasaro Sep 14, 2024
19b5f9f
Fixed CompetencyInformation test
raffifasaro Sep 14, 2024
5484e11
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 15, 2024
b6932b6
Added ExerciseTestRepository
raffifasaro Sep 16, 2024
557ba36
code improvements
raffifasaro Sep 16, 2024
8efbfd0
code review fixes
raffifasaro Sep 17, 2024
a800f5c
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 17, 2024
91f5e94
Merge remote-tracking branch 'origin/develop' into chore/learning-ana…
raffifasaro Sep 20, 2024
ae410d7
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 20, 2024
8097510
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 22, 2024
db4dec4
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 27, 2024
67404e3
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Sep 28, 2024
435ea12
Merge remote-tracking branch 'origin/develop' into chore/learning-ana…
raffifasaro Sep 30, 2024
f0da21e
resolve merge conflicts
raffifasaro Sep 30, 2024
d51b4b7
Apply suggestions from code review
raffifasaro Oct 1, 2024
272b155
fix israted error
raffifasaro Oct 1, 2024
a050d9a
Apply suggestions from code review
raffifasaro Oct 1, 2024
45f39d4
change fixes
raffifasaro Oct 1, 2024
921483e
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Oct 1, 2024
663c231
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
MaximilianAnzinger Oct 4, 2024
28850fb
replace exercise repository by test repository in tests
MaximilianAnzinger Oct 4, 2024
136d987
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Oct 5, 2024
3e848fa
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Oct 7, 2024
46597a7
requested change fix
raffifasaro Oct 7, 2024
bc729ea
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Oct 8, 2024
61bd9e7
Merge branch 'develop' into chore/learning-analytics/improve-metrics-…
raffifasaro Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ jacocoTestCoverageVerification {
counter = "CLASS"
value = "MISSEDCOUNT"
// TODO: in the future the following value should become less than 10
maximum = 40
maximum = 39
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.atlas.domain.competency.Competency;
import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyTaxonomy;

/**
Expand All @@ -19,4 +20,15 @@
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CompetencyInformationDTO(long id, String title, String description, CompetencyTaxonomy taxonomy, ZonedDateTime softDueDate, boolean optional, int masteryThreshold) {

/**
* Creates a CompetencyInformationDTO from a Competency.
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
*
* @param competency the Competency to create the DTO from
* @return the created DTO
*/
public static <C extends Competency> CompetencyInformationDTO of(C competency) {
return new CompetencyInformationDTO(competency.getId(), competency.getTitle(), competency.getDescription(), competency.getTaxonomy(), competency.getSoftDueDate(),
competency.isOptional(), competency.getMasteryThreshold());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record LectureUnitInformationDTO(long id, long lectureId, String lectureTitle, String name, ZonedDateTime releaseDate, Class<? extends LectureUnit> type) {

/**
* Creates a LectureUnitInformationDTO from a LectureUnit.
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
*
* @param lectureUnit the LectureUnit to create the DTO from
* @return the created DTO
*/
public static <L extends LectureUnit> LectureUnitInformationDTO of(L lectureUnit) {
return new LectureUnitInformationDTO(lectureUnit.getId(), lectureUnit.getLecture().getId(), lectureUnit.getLecture().getTitle(), lectureUnit.getName(),
lectureUnit.getReleaseDate(), lectureUnit.getClass());
}
}
162 changes: 148 additions & 14 deletions src/test/java/de/tum/cit/aet/artemis/MetricsIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
package de.tum.cit.aet.artemis;

import static de.tum.cit.aet.artemis.core.config.Constants.MIN_SCORE_GREEN;
import static de.tum.cit.aet.artemis.core.util.TimeUtil.toRelativeTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import java.time.Instant;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.util.ReflectionTestUtils;

import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.domain.ParticipantScore;
import de.tum.cit.aet.artemis.assessment.repository.StudentScoreRepository;
import de.tum.cit.aet.artemis.assessment.service.ParticipantScoreScheduleService;
import de.tum.cit.aet.artemis.atlas.dto.metrics.CompetencyInformationDTO;
import de.tum.cit.aet.artemis.atlas.dto.metrics.LectureUnitInformationDTO;
import de.tum.cit.aet.artemis.atlas.dto.metrics.ResourceTimestampDTO;
import de.tum.cit.aet.artemis.atlas.dto.metrics.StudentMetricsDTO;
import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository;
import de.tum.cit.aet.artemis.competency.CompetencyUtilService;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.Submission;
import de.tum.cit.aet.artemis.exercise.dto.ExerciseInformationDTO;
import de.tum.cit.aet.artemis.exercise.repository.ExerciseMetricsRepository;
import de.tum.cit.aet.artemis.exercise.repository.ExerciseTestRepository;
import de.tum.cit.aet.artemis.lecture.LectureUtilService;
import de.tum.cit.aet.artemis.lecture.repository.LectureUnitRepository;
import de.tum.cit.aet.artemis.lecture.service.LectureUnitService;

class MetricsIntegrationTest extends AbstractSpringIntegrationIndependentTest {

Expand All @@ -39,14 +48,40 @@ class MetricsIntegrationTest extends AbstractSpringIntegrationIndependentTest {
@Autowired
private ExerciseMetricsRepository exerciseMetricsRepository;

@Autowired
private CompetencyRepository competencyRepository;

@Autowired
private ExerciseTestRepository exerciseTestRepository;

@Autowired
private LectureUnitRepository lectureUnitRepository;

@Autowired
private StudentScoreRepository studentScoreRepository;

@Autowired
protected StudentScoreUtilService studentScoreUtilService;

@Autowired
protected LectureUtilService lectureUtilService;

@Autowired
protected CompetencyUtilService competencyUtilService;

@Autowired
protected LectureUnitService lectureUnitService;

private Course course;

private Course courseWithTestRuns;

private long userID;

private static final String STUDENT_OF_COURSE = TEST_PREFIX + "student1";

@BeforeEach
void setupTestScenario() {
void setupTestScenario() throws Exception {
// Prevents the ParticipantScoreScheduleService from scheduling tasks related to prior results
ReflectionTestUtils.setField(participantScoreScheduleService, "lastScheduledRun", Optional.of(Instant.now()));
ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 100;
Expand All @@ -56,8 +91,10 @@ void setupTestScenario() {

course = courseUtilService.createCourseWithAllExerciseTypesAndParticipationsAndSubmissionsAndResults(TEST_PREFIX, true);
courseWithTestRuns = courseUtilService.createCourseWithAllExerciseTypesAndParticipationsAndSubmissionsAndResultsAndTestRunsAndTwoUsers(TEST_PREFIX, true);
;
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved

userUtilService.createAndSaveUser(TEST_PREFIX + "user1337");
userID = userUtilService.getUserByLogin(TEST_PREFIX + "student1").getId();
}

@AfterEach
Expand Down Expand Up @@ -93,25 +130,56 @@ void shouldReturnExerciseInformation() throws Exception {
assertThat(exerciseInformation).allSatisfy((id, dto) -> assertThat(id).isEqualTo(dto.id()));
}

@Disabled // TODO: reduce jacoco missing by one after enabled
@Test
@WithMockUser(username = STUDENT_OF_COURSE, roles = "USER")
void shouldReturnAverageScores() throws Exception {
// Wait for the scheduler to execute its task
participantScoreScheduleService.executeScheduledTasks();
await().until(() -> participantScoreScheduleService.isIdle());
void shouldReturnCategories() throws Exception {
final var result = request.get("/api/metrics/course/" + course.getId() + "/student", HttpStatus.OK, StudentMetricsDTO.class);
assertThat(result).isNotNull();
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
assertThat(result.exerciseMetrics()).isNotNull();
final var categories = result.exerciseMetrics().categories();

final var expectedCategories = exerciseTestRepository.findAllWithCategoriesByCourseId(course.getId()).stream()
.collect(Collectors.toMap(Exercise::getId, Exercise::getCategories));

assertThat(categories).isEqualTo(expectedCategories);
}

@Test
@WithMockUser(username = STUDENT_OF_COURSE, roles = "USER")
void shouldReturnAverageScores() throws Exception {
final var exercises = exerciseTestRepository.findAllExercisesByCourseIdWithEagerParticipation(course.getId());
exercises.forEach(exercise -> studentScoreUtilService.createStudentScoreIsRated(exercise, userUtilService.getUserByLogin(STUDENT_OF_COURSE), 5));
final var result = request.get("/api/metrics/course/" + course.getId() + "/student", HttpStatus.OK, StudentMetricsDTO.class);
assertThat(result).isNotNull();
assertThat(result.exerciseMetrics()).isNotNull();
final var averageScores = result.exerciseMetrics().averageScore();

final var expectedAverageScores = exercises.stream().collect(Collectors.toMap(Exercise::getId,
exercise -> exercise.getStudentParticipations().stream().flatMap(participation -> participation.getStudents().stream()).mapToDouble(
student -> studentScoreRepository.findByExercise_IdAndUser_Id(exercise.getId(), student.getId()).map(ParticipantScore::getLastRatedScore).orElse(0.0))
.average().orElse(0.0)));

assertThat(averageScores).isEqualTo(expectedAverageScores);
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
@WithMockUser(username = STUDENT_OF_COURSE, roles = "USER")
void shouldReturnScore() throws Exception {
final var exercises = exerciseRepository.findAllExercisesByCourseId(course.getId());

final var expectedMap = exercises.stream().map(Exercise::getId).collect(
Collectors.toMap(Function.identity(), id -> resultRepository.findAllByParticipationExerciseId(id).stream().mapToDouble(Result::getScore).average().orElse(0)));
exercises.forEach(exercise -> studentScoreUtilService.createStudentScoreIsRated(exercise, userUtilService.getUserByLogin(STUDENT_OF_COURSE), 0.5));

assertThat(averageScores).isEqualTo(expectedMap);
final var result = request.get("/api/metrics/course/" + course.getId() + "/student", HttpStatus.OK, StudentMetricsDTO.class);
assertThat(result).isNotNull();
assertThat(result.exerciseMetrics()).isNotNull();
final var score = result.exerciseMetrics().score();

var expectedScores = exercises.stream()
.map(exercise -> studentScoreRepository.findByExercise_IdAndUser_Id(exercise.getId(), userID)
.map(studentScore -> Map.entry(exercise.getId(), studentScore.getLastRatedScore())))
.filter(Optional::isPresent).map(Optional::get).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

assertThat(score).isEqualTo(expectedScores);
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
Expand Down Expand Up @@ -181,7 +249,6 @@ void shouldFindLatestSubmissionDatesByUser() throws Exception {
Set<Long> exerciseIds = new HashSet<Long>();
final var exercises = exerciseRepository.findAllExercisesByCourseId(courseWithTestRuns.getId()).stream()
.map(exercise -> exerciseRepository.findWithEagerStudentParticipationsStudentAndSubmissionsById(exercise.getId()).orElseThrow());
final var userID = userUtilService.getUserByLogin(TEST_PREFIX + "student1").getId();

Set<ResourceTimestampDTO> expectedSet = exercises.flatMap(exercise -> {
exerciseIds.add(exercise.getId());
Expand All @@ -195,5 +262,72 @@ void shouldFindLatestSubmissionDatesByUser() throws Exception {
Set<ResourceTimestampDTO> result = exerciseMetricsRepository.findLatestSubmissionDatesForUser(exerciseIds, userID);
assertThat(result).isEqualTo(expectedSet);
}

@Test
@WithMockUser(username = STUDENT_OF_COURSE, roles = "USER")
void shouldReturnCompleted() throws Exception {
final var exercises = exerciseRepository.findAllExercisesByCourseId(course.getId());
exercises.forEach(exercise -> studentScoreUtilService.createStudentScoreIsRated(exercise, userUtilService.getUserByLogin(STUDENT_OF_COURSE), MIN_SCORE_GREEN));

final var result = request.get("/api/metrics/course/" + course.getId() + "/student", HttpStatus.OK, StudentMetricsDTO.class);
assertThat(result).isNotNull();
assertThat(result.exerciseMetrics()).isNotNull();
final var completed = result.exerciseMetrics().completed();

final var expectedCompleted = exercises.stream().map(Exercise::getId).filter(
id -> studentScoreRepository.findByExercise_IdAndUser_Id(id, userID).map(studentScore -> studentScore.getLastRatedScore() >= MIN_SCORE_GREEN).orElse(false))
.collect(Collectors.toSet());

assertThat(completed).isEqualTo(expectedCompleted);
}
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
}

@Nested
class CompetencyMetrics {

@Test
@WithMockUser(username = STUDENT_OF_COURSE, roles = "USER")
void shouldReturnCompetencyInformation() throws Exception {
course.setCompetencies(Set.of(competencyUtilService.createCompetency(course)));

final var result = request.get("/api/metrics/course/" + course.getId() + "/student", HttpStatus.OK, StudentMetricsDTO.class);
assertThat(result).isNotNull();
assertThat(result.competencyMetrics()).isNotNull();

final var competencyInformation = result.competencyMetrics().competencyInformation();

final var competencies = competencyRepository.findAllForCourse(course.getId());
final var expectedDTOs = competencies.stream().map(CompetencyInformationDTO::of).collect(Collectors.toSet());

assertThat(competencyInformation.values()).containsExactlyInAnyOrderElementsOf(expectedDTOs);
assertThat(competencyInformation).allSatisfy((id, dto) -> assertThat(id).isEqualTo(dto.id()));
}
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
}

@Nested
class LectureMetrics {

@Test
@WithMockUser(username = STUDENT_OF_COURSE, roles = "USER")
void shouldReturnLectureUnitInformation() throws Exception {

final var lectureUnit = lectureUtilService.createTextUnit();
lectureUnitService.linkLectureUnitsToCompetency(competencyUtilService.createCompetency(course), Set.of(lectureUnit), Set.of());

final var testLecture = lectureUtilService.createLecture(course, null);
lectureUtilService.addLectureUnitsToLecture(testLecture, List.of(lectureUnit));
course.addLectures(testLecture);

final var result = request.get("/api/metrics/course/" + course.getId() + "/student", HttpStatus.OK, StudentMetricsDTO.class);
assertThat(result).isNotNull();
assertThat(result.lectureUnitStudentMetricsDTO()).isNotNull();
final var lectureUnitInformation = result.lectureUnitStudentMetricsDTO().lectureUnitInformation();

final var lectureUnits = lectureUnitRepository.findAllById(Set.of(lectureUnit.getId()));
final var expectedDTOs = lectureUnits.stream().map(LectureUnitInformationDTO::of).collect(Collectors.toSet());

assertThat(lectureUnitInformation.values()).containsExactlyInAnyOrderElementsOf(expectedDTOs);
assertThat(lectureUnitInformation).allSatisfy((id, dto) -> assertThat(id).isEqualTo(dto.id()));
}
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
}
}
17 changes: 17 additions & 0 deletions src/test/java/de/tum/cit/aet/artemis/StudentScoreUtilService.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ public void createStudentScore(Exercise exercise, User user, double score) {
studentScoreRepository.save(studentScore);
}

/**
* Creates rated and normal score (which are set equal) for given exercise and user.
*
* @param exercise the exercise to link the student score to
* @param user the user that is linked to the score
* @param score the score and rated score that the specified user has reached for the given exercise
*/
public void createStudentScoreIsRated(Exercise exercise, User user, double score) {
final var studentScore = new StudentScore();
studentScore.setExercise(exercise);
studentScore.setUser(user);
studentScore.setLastScore(score);
studentScore.setLastPoints(exercise.getMaxPoints() * score / 100);
studentScore.setLastRatedScore(score);
studentScoreRepository.save(studentScore);
}

/**
* Creates student score for given exercise and user.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package de.tum.cit.aet.artemis.exercise.repository;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.List;
import java.util.Set;

import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;

/**
* Spring Data JPA repository for the Exercise entity for Tests.
*/
@Profile(PROFILE_CORE)
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
@Repository
public interface ExerciseTestRepository extends ArtemisJpaRepository<Exercise, Long> {
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved

@EntityGraph(attributePaths = { "studentParticipations", "studentParticipations.student", "studentParticipations.submissions" })
@Query("""
SELECT e
FROM Exercise e
WHERE e.course.id = :courseId
""")
Set<Exercise> findAllExercisesByCourseIdWithEagerParticipation(@Param("courseId") Long courseId);

@EntityGraph(attributePaths = "categories")
List<Exercise> findAllWithCategoriesByCourseId(Long courseId);
}
raffifasaro marked this conversation as resolved.
Show resolved Hide resolved
Loading