Imagine I want my MapStruct mappers to be Spring-managed @Component
s (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 @Bean
s 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
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 {