Skip to content

Commit

Permalink
add exam service and end exam action
Browse files Browse the repository at this point in the history
  • Loading branch information
pionas committed Dec 16, 2023
1 parent 95b5ef9 commit e435b19
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/main/java/info/pionas/quiz/domain/exam/ExamFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package info.pionas.quiz.domain.exam;


import info.pionas.quiz.domain.exam.api.AnswerForQuestionNotFoundException;
import info.pionas.quiz.domain.exam.api.ExamAnswer;
import info.pionas.quiz.domain.exam.api.ExamDetails;
import info.pionas.quiz.domain.exam.api.PassExamAnswer;
import info.pionas.quiz.domain.quiz.api.Question;
import info.pionas.quiz.domain.quiz.api.Quiz;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@Component
@NoArgsConstructor(access = AccessLevel.PACKAGE)
class ExamFactory {

public ExamDetails endExam(String username, Quiz quiz, List<PassExamAnswer> answers) {
return ExamDetails.of(username, quiz, getAnswerResult(quiz.getQuestions(), answers));
}

private List<ExamAnswer> getAnswerResult(List<Question> questions, List<PassExamAnswer> answers) {
return Optional.ofNullable(questions)
.orElseGet(Collections::emptyList)
.stream()
.map(question -> ExamAnswer.of(question, getAnswerForQuestion(question, answers)))
.toList();
}

private PassExamAnswer getAnswerForQuestion(Question question, List<PassExamAnswer> answers) {
final var questionId = question.getId();
return Optional.ofNullable(answers)
.orElseGet(Collections::emptyList)
.stream()
.filter(passExamAnswer -> passExamAnswer.isAnswerForQuestion(questionId))
.findAny()
.orElseThrow(() -> new AnswerForQuestionNotFoundException(questionId));
}
}
31 changes: 31 additions & 0 deletions src/main/java/info/pionas/quiz/domain/exam/ExamServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package info.pionas.quiz.domain.exam;

import info.pionas.quiz.domain.exam.api.PassExamAnswer;
import info.pionas.quiz.domain.exam.api.ExamRepository;
import info.pionas.quiz.domain.exam.api.ExamResult;
import info.pionas.quiz.domain.exam.api.ExamService;
import info.pionas.quiz.domain.quiz.QuizNotFoundException;
import info.pionas.quiz.domain.quiz.api.QuizRepository;
import info.pionas.quiz.domain.shared.UuidGenerator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;

@Service
@AllArgsConstructor
class ExamServiceImpl implements ExamService {

private final QuizRepository quizRepository;
private final ExamRepository examRepository;
private final ExamFactory examFactory;
private final UuidGenerator uuidGenerator;

@Override
public ExamResult endExam(String username, UUID quizId, List<PassExamAnswer> answers) {
final var quiz = quizRepository.findById(quizId)
.orElseThrow(() -> new QuizNotFoundException(quizId));
return examRepository.save(uuidGenerator.generate(), examFactory.endExam(username, quiz, answers));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package info.pionas.quiz.domain.exam.api;

import info.pionas.quiz.domain.shared.exception.NotFoundException;

import java.util.UUID;

public class AnswerForQuestionNotFoundException extends NotFoundException {
public AnswerForQuestionNotFoundException(UUID questionId) {
super(String.format("There is no answer to the question %s", questionId));
}
}
24 changes: 24 additions & 0 deletions src/main/java/info/pionas/quiz/domain/exam/api/ExamAnswer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package info.pionas.quiz.domain.exam.api;

import info.pionas.quiz.domain.quiz.api.Question;
import lombok.Builder;
import lombok.Getter;

import java.util.UUID;

@Getter
@Builder
public class ExamAnswer {

private UUID questionId;
private UUID answerId;
private Boolean correct;

public static ExamAnswer of(Question question, PassExamAnswer answerForQuestion) {
return ExamAnswer.builder()
.questionId(question.getId())
.answerId(answerForQuestion.getAnswerId())
.correct(question.isCorrectAnswer(answerForQuestion))
.build();
}
}
18 changes: 18 additions & 0 deletions src/main/java/info/pionas/quiz/domain/exam/api/ExamDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package info.pionas.quiz.domain.exam.api;

import info.pionas.quiz.domain.quiz.api.Quiz;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Builder
@Getter
@AllArgsConstructor(staticName = "of")
public class ExamDetails {

private String username;
private Quiz quiz;
private List<ExamAnswer> answers;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package info.pionas.quiz.domain.exam.api;

import java.util.UUID;

public interface ExamRepository {

ExamResult save(UUID id, ExamDetails examDetails);
}
23 changes: 23 additions & 0 deletions src/main/java/info/pionas/quiz/domain/exam/api/ExamResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package info.pionas.quiz.domain.exam.api;

import info.pionas.quiz.domain.quiz.api.Quiz;
import lombok.Builder;
import lombok.Getter;

import java.util.List;
import java.util.UUID;

@Builder
@Getter
public class ExamResult {

private UUID id;
private Quiz quiz;
private List<ExamAnswer> answers;

public long getCorrectAnswer() {
return answers.stream()
.filter(ExamAnswer::getCorrect)
.count();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package info.pionas.quiz.domain.exam.api;

import java.util.List;
import java.util.UUID;

public interface ExamService {

ExamResult endExam(String username, UUID uuid, List<PassExamAnswer> answers);
}
19 changes: 19 additions & 0 deletions src/main/java/info/pionas/quiz/domain/exam/api/PassExamAnswer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package info.pionas.quiz.domain.exam.api;

import lombok.Builder;
import lombok.Getter;

import java.util.Objects;
import java.util.UUID;

@Getter
@Builder
public class PassExamAnswer {

private UUID questionId;
private UUID answerId;

public boolean isAnswerForQuestion(UUID questionId) {
return Objects.equals(this.questionId, questionId);
}
}
4 changes: 4 additions & 0 deletions src/main/java/info/pionas/quiz/domain/quiz/api/Answer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Getter;
import lombok.Setter;

import java.util.Objects;
import java.util.UUID;

@Getter
Expand All @@ -15,4 +16,7 @@ public class Answer {
private String content;
private boolean correct;

public boolean isAnswerFor(UUID answerId) {
return Objects.equals(id, answerId);
}
}
7 changes: 7 additions & 0 deletions src/main/java/info/pionas/quiz/domain/quiz/api/Question.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package info.pionas.quiz.domain.quiz.api;

import info.pionas.quiz.domain.exam.api.PassExamAnswer;
import lombok.Builder;
import lombok.Getter;

Expand All @@ -21,4 +22,10 @@ public void update(String content, List<Answer> updateAnswers) {
this.answers.clear();
this.answers.addAll(updateAnswers);
}

public boolean isCorrectAnswer(PassExamAnswer passExamAnswer) {
return answers.stream()
.filter(answer -> answer.isAnswerFor(passExamAnswer.getAnswerId()))
.allMatch(Answer::isCorrect);
}
}
143 changes: 143 additions & 0 deletions src/test/java/info/pionas/quiz/domain/exam/ExamServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package info.pionas.quiz.domain.exam;

import info.pionas.quiz.domain.exam.api.*;
import info.pionas.quiz.domain.quiz.QuizNotFoundException;
import info.pionas.quiz.domain.quiz.api.Answer;
import info.pionas.quiz.domain.quiz.api.Question;
import info.pionas.quiz.domain.quiz.api.Quiz;
import info.pionas.quiz.domain.quiz.api.QuizRepository;
import info.pionas.quiz.domain.shared.UuidGenerator;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

class ExamServiceTest {

private final QuizRepository quizRepository = Mockito.mock(QuizRepository.class);
private final ExamRepository examRepository = Mockito.mock(ExamRepository.class);
private final UuidGenerator uuidGenerator = Mockito.mock(UuidGenerator.class);
private final ExamFactory examFactory = new ExamFactory();
private final ExamService service = new ExamServiceImpl(quizRepository, examRepository, examFactory, uuidGenerator);

@Test
void should_throw_not_found_exception_when_quiz_by_id_not_exist() {
//given
final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63");
final var answers = Collections.<PassExamAnswer>emptyList();
when(quizRepository.findById(quizId)).thenReturn(Optional.empty());
//when
QuizNotFoundException exception = assertThrows(QuizNotFoundException.class, () -> service.endExam("username", quizId, answers));
//then
assertThat(exception).isNotNull();
assertThat(exception.getMessage())
.isEqualTo(String.format("Quiz by ID %s not exist", quizId));
}

@Test
void should_throw_answer_for_question_not_found_exception_when_question_has_not_answer() {
//given
final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63");
final var answers = Collections.<PassExamAnswer>emptyList();
when(quizRepository.findById(quizId)).thenReturn(Optional.of(getQuiz(quizId)));
//when
AnswerForQuestionNotFoundException exception = assertThrows(AnswerForQuestionNotFoundException.class, () -> service.endExam("username", quizId, answers));
//then
assertThat(exception).isNotNull();
assertThat(exception.getMessage())
.isEqualTo("There is no answer to the question 03df4c0e-a760-4b23-aebe-ac0fd8761804");
}

@Test
void should_end_exam() {
//given
final var examResultId = UUID.fromString("7a398eb6-1d20-4a05-b13b-c752c3c7c5d3");
final var quizId = UUID.fromString("b83d5c22-7b78-4435-9daa-17bb532c0f63");
final var answers = getAnswers();
when(quizRepository.findById(quizId)).thenReturn(Optional.of(getQuiz(quizId)));
when(examRepository.save(Mockito.any(UUID.class), Mockito.any(ExamDetails.class))).thenReturn(createExamResult(examResultId, getQuiz(quizId), answers));
when(uuidGenerator.generate()).thenReturn(examResultId);
//when
ExamResult examResult = service.endExam("username", quizId, answers);
//then
assertThat(examResult).isNotNull();
assertThat(examResult.getId()).isEqualTo(examResultId);
assertThat(examResult.getCorrectAnswer()).isEqualTo(1L);
}

private Quiz getQuiz(UUID quizId) {
final var questionId1 = UUID.fromString("03df4c0e-a760-4b23-aebe-ac0fd8761804");
final var questionId2 = UUID.fromString("0feb6e91-a25c-4c9a-8f6c-acf489a5af6b");
final var answerId1 = UUID.fromString("1f12ef49-9a35-45f2-9824-edb673602a7f");
final var answerId2 = UUID.fromString("126b0faa-1de1-4b9e-b79c-8b03361e8839");
final var answerId3 = UUID.fromString("9f5e41ad-9614-4a14-ac8a-dad55d4ce89f");
final var answerId4 = UUID.fromString("5e02b9e3-c1d4-43da-b7df-45899da5b9a2");
final var content1 = "Content 1";
final var content2 = "Content 2";
final var content3 = "Content 3";
final var content4 = "Content 4";

final var question1 = Question.builder()
.id(questionId1)
.content("Spring is the best JAVA framework")
.answers(new ArrayList<>(Arrays.asList(createAnswer(answerId1, content1, true), createAnswer(answerId2, content2, false))))
.build();
final var question2 = Question.builder()
.id(questionId2)
.content("What is the best Front End framework")
.answers(new ArrayList<>(Arrays.asList(createAnswer(answerId3, content3, false), createAnswer(answerId4, content4, true))))
.build();
return Quiz.builder()
.id(quizId)
.title("Title")
.description("Description")
.questions(new ArrayList<>(Arrays.asList(question1, question2)))
.build();
}

private Answer createAnswer(UUID id, String content, Boolean correct) {
return Answer.builder()
.id(id)
.content(content)
.correct(correct)
.build();
}

private List<PassExamAnswer> getAnswers() {
final var questionId1 = UUID.fromString("03df4c0e-a760-4b23-aebe-ac0fd8761804");
final var answerId1 = UUID.fromString("1f12ef49-9a35-45f2-9824-edb673602a7f");
final var questionId2 = UUID.fromString("0feb6e91-a25c-4c9a-8f6c-acf489a5af6b");
final var answerId2 = UUID.fromString("9f5e41ad-9614-4a14-ac8a-dad55d4ce89f");
return List.of(
createPassExamAnswer(questionId1, answerId1),
createPassExamAnswer(questionId2, answerId2)
);
}

private PassExamAnswer createPassExamAnswer(UUID questionId, UUID answerId) {
return PassExamAnswer.builder()
.questionId(questionId)
.answerId(answerId)
.build();
}

private ExamResult createExamResult(UUID examResultId, Quiz quiz, List<PassExamAnswer> answers) {
final var questions = quiz.getQuestions();
return ExamResult.builder()
.id(examResultId)
.quiz(quiz)
.answers(List.of(
crateExamAnswer(questions.getFirst(), answers.getFirst()),
crateExamAnswer(questions.getLast(), answers.getLast())
))
.build();
}

private ExamAnswer crateExamAnswer(Question question, PassExamAnswer passExamAnswer) {
return ExamAnswer.of(question, passExamAnswer);
}
}

0 comments on commit e435b19

Please sign in to comment.