javaspringmapstruct

Making MapStruct put @Component on its implementations – while allowing for Spring-less initialization


Imagine I want my MapStruct mappers to be Spring-managed @Components (for production).

Also imagine I want to test those mappers. However, I don't want to bootstrap the entire Spring context to do that. Or even a tiny context that has the mappers only. I want to unit-test the mappers and keep Spring completely unaware of that.

As far as I know, the only way to make MapStruct annotate its generated mapper implementations with @Component is to set componentModel to "spring".

However, that means that any mappers created with the Mappers factory (which I want to do in my unit-test) would not be initialized.

So it seems I'm between a rock and a hard place: I either manually create mapper @Beans or abandon the idea of testing MapStruct mappers without Spring's involvement. Or am I wrong?

import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class UserRequestDto {

    private Long id;
    private String name;
    private List<String> emailData;
}
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "\"user\"")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @OneToMany
    private List<EmailData> emailData;
}
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "email_data")
public class EmailData {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // a back reference to user is omitted for irrelevance
    private String email;
}
import org.mapstruct.Mapper;

@Mapper(uses = EmailMapper.class, componentModel="spring")
public interface UserMapper {
    User toUser(UserRequestDto userRequestDto);
}
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface EmailMapper {
    @Mapping(source = ".", target = "email")
    EmailData toEmailData(String email);
}
import com.example.pixel_user_api.data.dto.request.UserRequestDto;
import com.example.pixel_user_api.data.entity.User;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class UserMapperTest {
    @Test
    void testMapper() {
        UserRequestDto userDto = new UserRequestDto();
        userDto.setId(123L);
        userDto.setName("Steve123");
        String email = "steve@gmail.com";
        userDto.setEmailData(List.of(email));

        UserMapper mapper = Mappers.getMapper(UserMapper.class);
        User user = mapper.toUser(userDto); // throws NPE
    }
}
java.lang.NullPointerException: Cannot invoke "com.example.pixel_user_api.mapper.EmailMapper.toEmailData(String)" because "this.emailMapper" is null

Solution

  • You can always use constructor injection and just instantiate it in your tests.

    e.g.

    @Mapper(
        uses = EmailMapper.class,
        componentModel = "spring",
       injectionStrategy = InjectionStrategy.CONSTRUCTOR
    )
    
    public interface UserMapper {