6
respostas

Ajuda com um Test

Estou enfrentando um erro que não entendi como resolver:

Tenho esse método e quero testar se está salvando o course.

@Override
@Transactional
public CourseCreateResponse createCourse(CourseCreateRequest request){
    createValidators.forEach(v -> v.validate(request));

    var instructor = userRepository.getReferenceById(request.instructorId());
    var course = new Course(request);
    course.setInstructor(instructor);
    return new CourseCreateResponse(courseRepository.save(course));
}

Criei o seguite teste

@ExtendWith(MockitoExtension.class)
public class CourseServiceTestB {

    @InjectMocks
    private CourseServiceImpl service;

    @Mock
    private UserRepository userRepository;

    @Mock
    private CourseRepository courseRepository;

    @Mock
    private User instructor;

    @Mock
    private Course course;
    
    private CourseCreateRequest request;

    @Captor
    private ArgumentCaptor<Course> courseCaptor;

    @Test
    void shoulSaveCourse() {

        this.request = new CourseCreateRequest("curso A", "curso-a", "Curso A", 1L);
        given(course.getId()).willReturn(1L);
        given(userRepository.getReferenceById(request.instructorId())).willReturn(instructor);

        service.createCourse(request);

        then(courseRepository).should().save(courseCaptor.capture());
        var savedCourse = courseCaptor.getValue();

        Assertions.assertEquals(instructor, savedCourse.getInstructor());
        Assertions.assertEquals(request.name(), savedCourse.getName());
        Assertions.assertEquals(request.code(), savedCourse.getCode());
        Assertions.assertEquals(request.description(), savedCourse.getDescription());
        Assertions.assertEquals(Status.ACTIVE, savedCourse.getStatus());
        Assertions.assertEquals(LocalDate.now(), savedCourse.getCreatedAt());
    }

E estou tomando esse erro:

java.lang.NullPointerException: Cannot invoke "io.github.enrolmentsystem.domain.course.Course.getId()" because "course" is null
    at io.github.enrolmentsystem.domain.course.response.CourseCreateResponse.<init>(CourseCreateResponse.java:8)
    at io.github.enrolmentsystem.service.impl.CourseServiceImpl.createCourse(CourseServiceImpl.java:46)
    at io.github.enrolmentsystem.course.service.CourseServiceTestB.shoulSaveCourse(CourseServiceTestB.java:60)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
6 respostas

Oi!

Manda aqui o código das suas classes Course e CourseCreateResponse.

Seguem:

package io.github.enrolmentsystem.domain.course.response;

import io.github.enrolmentsystem.domain.course.Course;
import io.github.enrolmentsystem.domain.course.Status;

public record CourseCreateResponse(Long id, String name, String code, Status status, String instructorName) {
    public CourseCreateResponse(Course course) {
        this(course.getId(), course.getName(), course.getCode(), course.getStatus(), course.getInstructor().getName());
    }
}
package io.github.enrolmentsystem.domain.course;

import io.github.enrolmentsystem.domain.course.request.CourseCreateRequest;
import io.github.enrolmentsystem.domain.enrolment.Enrolment;
import io.github.enrolmentsystem.domain.evaluation.CourseEvaluation;
import io.github.enrolmentsystem.domain.user.User;
import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Entity
@Table
@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode(of = "code")
public class Course  {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    private String name;

    @Setter
    private String code;

    @Setter
    private String description;

    @Setter
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User instructor;

    @Setter
    @Enumerated(EnumType.STRING)
    private Status status;

    @Setter
    private LocalDate createdAt;

    @Setter
    private LocalDate inactivatedAt;

    @OneToMany(mappedBy = "course")
    private List<Enrolment> enrolments = new ArrayList<>();

    @OneToMany(mappedBy = "course")
    private List<CourseEvaluation> courseEvaluations = new ArrayList<>();

    public Course(CourseCreateRequest request) {
        this.status = Status.ACTIVE;
        this.name = request.name();
        this.code = request.code();
        this.description = request.description();
        this.createdAt = LocalDate.now();
        this.inactivatedAt = null;

    }
    public void inactivateCourse(){
        this.status = Status.INACTIVE;
        this.inactivatedAt = LocalDate.now();
    }

    public void addEvaluation(CourseEvaluation evaluation){
        this.courseEvaluations.add(evaluation);
    }

    public void addEnrolment(Enrolment enrolment){
        this.enrolments.add(enrolment);
    }


}

Oi Gibran!

Analisando o seu código, a recomendação seria você testar o retorno do método, ou seja, o objeto CourseCreateResponse, ao invés do objeto Course.

No curso utilizamos o ArgumentCaptor porque o método da service era void. Então, houve a necessidade de capturar o objeto sendo criado dentro desse método via captor. Mas no seu caso, o método devolve o DTO e os asserts podem ser feitos nele, para garantir que ele foi criado corretamente, de acordo com os valores do Course sendo criado no método testado.

Reescrevi assim e passou... mas ainda acho que estou com o racional errado para escrever os testes:

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Os testes devem validar as saídas de acordo com as entradas fornecidas, independente da implementação, pois assim não se cria um acoplamento entre o código de teste e o código de implementação em si, facilitando com isso a manutenção futura.

No seu exemplo, a entrada é o CourseCreateRequest e a saída é o CourseCreateResponse. Mas como o método sendo testado tem dependências externas (interfaces repository que acessam o banco de dados), é necessário utilizar mocks para "fingir" que o banco de dados foi acessado e com isso ser possível simular vários cenário distintos.

Pense em cada cenário de teste como: "Dado que eu forneça estas informações (entrada) e o sistema esteja no estado X, este é o resultado esperado (saída)"

Exemplo:

  1. Entrada: {"name": "Curso XPTO", "code": "curso-xpto", "description": "Descrição curso XPTO", "instructorId": 1}
  2. Estado atual do sistema: Nenhum curso cadastrado e instrutor (id: 1; nome: "João Silva") cadastrado
  3. Saída esperada: {"name": "Curso XPTO", "code": "curso-xpto", "status": "ACTIVE", "instructorName": "João Silva"}

Traduzindo para código:

@Test
void shoulSaveCourse() {
    // 1. ENTRADA PARA ESTE CENARIO:
    
    var request = new CourseCreateRequest("Curso XPTO", "curso-xpto", "Descrição curso XPTO", 1L);
    
    // 2. ESTADO ATUAL DO SISTEMA PARA ESTE CENARIO:
    
    given(instructor.getName()).willReturn("João da Silva");
    given(userRepository.getReferenceById(request.instructorId())).willReturn(instructor);
    given(courseRepository.save(any())).willReturn(new Course(request));

    // 3. CHAMADA DO METODO SENDO TESTADO:
    
    var response = service.createCourse(request);

    // 4. SAIDA ESPERADA PARA ESTE CENARIO:
    
    assertEquals(request.name(), response.name());
    assertEquals(request.code(), response.code());
    assertEquals(Status.ACTIVE, response.getStatus());
    assertEquals(instructor.getName(), response.instructorName());
    
    // precisa verificar se o repository foi chamado para salvar no banco, pios o service pode apenas estar retornando o objeto response sem de fato salvar no banco
    then(courseRepository).save(new Course(request));
}

Certo. ainda tem essa parte aqui: given(courseRepository.save(any())).willReturn(new Course(request))

no serviço, primeiro é instanciado um curse s partir do request e depois faz o course.setInstructor. só então o repositsory é chamado para salvar.