Search code examples
javaspring-bootunit-testingjunit5spring-boot-test

Spring boot is throwing my custom exception in my service unit test yet I see no reason why it is doing that


So I have the small app that I am working on it has 3 entities Device, StakeLimit (which has Device Foreign key) and TicketMessage (this one is irrelevant for the problem), and it has couple of services, controllers etc. The problem is in StakeLimitService specifically in StakeLimitServiceTest unit test that I am writing. The problem is in method that I am testing it keeps throwing my custom exception DeviceNotFoundException which is thrown when device by provided id is not found in repository and the problem there is it should not throw exception yet it does. I tried everything to get rid of it including using chatGPT in hope of finding the solution and I only got it to work with one solution which makes no sense to me why it is working yet my original one is not (this I provided at the bottom).

This is my code: Device:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "Device")
@Table(name = "devices")
public class Device {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(
            name = "id",
            updatable = false
    )
    private UUID id;

    @Column(name = "blocked")
    private boolean blocked;

    @Column(name = "restriction_expires")
    private boolean restrictionExpires;

    @Column(name = "restriction_expires_at")
    private LocalDateTime restrictionExpiresAt;
}

DeviceRepository:

@Repository
public interface DeviceRepository extends JpaRepository<Device, UUID> {

    Device findDeviceById(UUID id);
}

StakeLimit:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "StakeLimit")
@Table(name = "stake_limits")
public class StakeLimit {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(
            name = "id",
            updatable = false
    )
    private UUID id;

    @ManyToOne
    @JoinColumn(
            name = "device_id",
            nullable = false,
            referencedColumnName = "id",
            foreignKey = @ForeignKey(
                    name = "device_id_fk"
            )
    )
    private Device device;

    @Column(
            name = "time_duration",
            nullable = false
    )
    private Integer timeDuration;

    @Column(
            name = "stake_limit",
            nullable = false
    )
    private Double stakeLimit;

    @Column(
            name = "hot_amount_pctg",
            nullable = false
    )
    private Integer hotAmountPctg;

    @Column(
            name = "restr_expiry",
            nullable = false
    )
    private Integer restrExpiry;
}

StakeLimitRepository:

public interface StakeLimitRepository extends JpaRepository<StakeLimit, UUID> {

    boolean existsByDevice(Device device);
    StakeLimit findByDevice(Device device);

}

StakeLimitRequest:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StakeLimitRequest {
    //Using String instead of UUID because I could not get my custom validation to work
    @NotEmpty(message = "DeviceId field can't be empty")
    private String deviceId;

    @Min(value = 300, message = "Minimum time duration is 5 minutes (300 seconds)")
    @Max(value = 86400, message = "Maximum time duration is 24 hours (86400 seconds)")
    private Integer timeDuration;

    @Min(value = 1, message = "Minimum stake limit is 1")
    @Max(value = 10000000, message = "Maximum stake limit is 10000000")
    private Double stakeLimit;

    @Min(value = 1, message = "Minimum hot amount percentage is 1")
    @Max(value = 100, message = "Maximum hot amount percentage is 100")
    private Integer hotAmountPctg;

    @Min(value = 0, message = "Minimum restriction expiry is 1 minute (60 seconds)" +
            " while maximum is 0 seconds (it never expires)")
    private Integer restrExpiry;
}

StakeLimitResponse:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StakeLimitResponse {
    private Integer timeDuration;
    private Double stakeLimit;
    private Integer hotAmountPctg;
    private Integer restrExpiry;
}

StakeLimitService:

@Service
@RequiredArgsConstructor
public class StakeLimitService {

    private final StakeLimitRepository stakeLimitRepository;
    private final DeviceRepository deviceRepository;

    public StakeLimitResponse getStakeLimit(String deviceId) {
        //method for checking if provided deviceId is actually UUID (for more info
        //check comments above isUuidValid method)
        UUID deviceUuid = isUuidValid(deviceId);
        //then check if device by that id exists in our devices table
        if (!deviceRepository.existsById(deviceUuid)) {
            //if it does not throw exp
            throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
        }
        //else check if that device contains stake limits
        if (!stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceUuid))) {
            //if not throw exp
            throw new StakeLimitNotFoundException("Stake limit with device " + deviceUuid + " id not found");
        }

        //else get device and then get our stake limit using that device
        var device = deviceRepository.findDeviceById(deviceUuid);
        var stakeLimit = stakeLimitRepository.findByDevice(device);
        //and finally build our response and return it to user
        return StakeLimitResponse.builder()
                .timeDuration(stakeLimit.getTimeDuration())
                .stakeLimit(stakeLimit.getStakeLimit())
                .hotAmountPctg(stakeLimit.getHotAmountPctg())
                .restrExpiry(stakeLimit.getRestrExpiry())
                .build();
    }

    public StakeLimitResponse addStakeLimit(StakeLimitRequest request) {
        //same as above
        UUID deviceId = isUuidValid(request.getDeviceId());
        //Could also make it if deviceId does not exist, create device with that id
        if (!deviceRepository.existsById(deviceId)) {
            throw new DeviceNotFoundException("Device with " + deviceId + " id not found");
        }
        if (stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceId))) {
            throw new DeviceAlreadyExistsException(
                    "Stake limit with device " + deviceId + " id already exists"
            );
        }
        var device = deviceRepository.findDeviceById(deviceId);
        //when all checks are completed we finally check if restriction is set to
        //expire or not and set it to our device and save it to our db again
        device.setRestrictionExpires(request.getRestrExpiry() != 0);
        deviceRepository.save(device);

        //then we build our stake limit and save it to db
        var stakeLimit = StakeLimit.builder()
                .device(device)
                .timeDuration(request.getTimeDuration())
                .stakeLimit(request.getStakeLimit())
                .hotAmountPctg(request.getHotAmountPctg())
                .restrExpiry(request.getRestrExpiry())
                .build();
        stakeLimitRepository.save(stakeLimit);

        //finally build our response and return it
        return StakeLimitResponse.builder()
                .timeDuration(stakeLimit.getTimeDuration())
                .stakeLimit(stakeLimit.getStakeLimit())
                .hotAmountPctg(stakeLimit.getHotAmountPctg())
                .restrExpiry(stakeLimit.getRestrExpiry())
                .build();
    }

    @Transactional
    public StakeLimitResponse changeStakeLimit(
            String deviceId,
            Integer timeDuration,
            Double stakeLimit,
            Integer hotAmountPctg,
            Integer restrExpiry
    ) {
        //same checks as the above
        UUID deviceUuid = isUuidValid(deviceId);
        if (!deviceRepository.existsById(deviceUuid)) {
            throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
        }
        if (!stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceUuid))) {
            throw new StakeLimitNotFoundException("Stake limit for device with id " + deviceUuid + " not found");
        }
        var device = deviceRepository.findDeviceById(deviceUuid);
        var stakeLimitDB = stakeLimitRepository.findByDevice(device);

        //then we check which fields user actually sent (decided to change) and change them
        if (timeDuration != null && !timeDuration.equals(stakeLimitDB.getTimeDuration())) {
            stakeLimitDB.setTimeDuration(timeDuration);
        }
        if (stakeLimit != null && !stakeLimit.equals(stakeLimitDB.getStakeLimit())) {
            stakeLimitDB.setStakeLimit(stakeLimit);
        }
        if (hotAmountPctg != null && !hotAmountPctg.equals(stakeLimitDB.getHotAmountPctg())) {
            stakeLimitDB.setHotAmountPctg(hotAmountPctg);
        }
        if (restrExpiry != null && !restrExpiry.equals(stakeLimitDB.getRestrExpiry())) {
            device.setRestrictionExpires(restrExpiry != 0);
            deviceRepository.save(device);
            stakeLimitDB.setRestrExpiry(restrExpiry);
        }
        stakeLimitRepository.save(stakeLimitDB);

        return StakeLimitResponse.builder()
                .timeDuration(stakeLimitDB.getTimeDuration())
                .stakeLimit(stakeLimitDB.getStakeLimit())
                .hotAmountPctg(stakeLimitDB.getHotAmountPctg())
                .restrExpiry(stakeLimitDB.getRestrExpiry())
                .build();
    }

    //method used for checking if provided UUID is correct
    //because I could not get custom validation to work properly I switched all requests
    //to take String instead of pure UUID, and then i check if that String matches
    //UUID pattern, if it does not match pattern throw custom exception else
    //transform provided String into actual UUID
    private UUID isUuidValid(String uuid) {
        final String uuid_pattern = "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$";

        if (!uuid.matches(uuid_pattern)) {
            throw new UuidNotValidException(uuid + " UUID not valid");
        }

        return UUID.fromString(uuid);
    }
}

StakeLimitServiceTest:

@ExtendWith(MockitoExtension.class)
class StakeLimitServiceTest {

    @Mock
    private StakeLimitRepository stakeLimitRepository;

    @Mock
    private DeviceRepository deviceRepository;

    @InjectMocks
    private StakeLimitService stakeLimitService;

    private StakeLimit stakeLimit;
    private StakeLimitRequest stakeLimitRequest;
    private StakeLimitResponse stakeLimitResponse;
    private Device device;

    @BeforeEach
    void setUp() {
        device = Device.builder().id(UUID.fromString("799de2ee-13c2-40a1-8230-d7318de97925")).build();
        stakeLimit = StakeLimit.builder()
                .device(device)
                .timeDuration(1800)
                .stakeLimit(999.0)
                .hotAmountPctg(80)
                .restrExpiry(300)
                .build();
        stakeLimitRequest = StakeLimitRequest.builder()
                .deviceId(device.getId().toString())
                .timeDuration(1800)
                .stakeLimit(999.0)
                .hotAmountPctg(80)
                .restrExpiry(300)
                .build();
        stakeLimitResponse = StakeLimitResponse.builder()
                .timeDuration(1800)
                .stakeLimit(999.0)
                .hotAmountPctg(80)
                .restrExpiry(300)
                .build();
    }

    @Test
    void Should_ReturnStakeLimitResponse_When_GetStakeLimit() {
        // when
        when(deviceRepository.findDeviceById(device.getId())).thenReturn(device);
        when(stakeLimitRepository.findByDevice(device)).thenReturn(stakeLimit);

        StakeLimitResponse response = stakeLimitService.getStakeLimit(device.getId().toString());

        // then
        assertThat(response).isNotNull();
        verify(deviceRepository).findDeviceById(any());
        verify(stakeLimitRepository).findByDevice(any());
    }

    @Disabled
    @Test
    void Should_CreateStakeLimitAndReturnStakeLimitResponse_When_AddStakeLimit() {
        // given
        stakeLimitRequest.setDeviceId(device.getId().toString());
        // when
        when(deviceRepository.findDeviceById(device.getId())).thenReturn(device);
        when(stakeLimitRepository.save(any(StakeLimit.class))).thenReturn(stakeLimit);

        StakeLimitResponse savedStakeLimit = stakeLimitService.addStakeLimit(stakeLimitRequest);

        // then
        assertThat(savedStakeLimit).isNotNull();
        verify(deviceRepository).findDeviceById(any());
        verify(stakeLimitRepository).save(any());
    }

    @Disabled
    @Test
    void Should_ChangeStakeLimitAndReturnStakeLimitResponse_When_ChangeStakeLimit() {
    }
}

Also for some reason when I just try to create new device Device device = new Device() my device has null for id rather than auto generating UUID and that is also why I am building the device myself and passing it UUID.

The only thing that worked is when I switched

UUID deviceUuid = isUuidValid(deviceId);
        if (!deviceRepository.existsById(deviceUuid)) {
            throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
        }
        if (!stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceUuid))) {
            throw new StakeLimitNotFoundException("Stake limit with device " + deviceUuid + " id not found");
        }

        var device = deviceRepository.findDeviceById(deviceUuid);
        var stakeLimit = stakeLimitRepository.findByDevice(device);

For

Device device = deviceRepository.findDeviceById(deviceId);
        UUID deviceUuid = isUuidValid(deviceId);
        Device device = deviceRepository.findDeviceById(deviceUuid);
        if (device == null) {
            throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
        }

        StakeLimit stakeLimit = stakeLimitRepository.findByDevice(device);
        if (stakeLimit == null) {
            throw new StakeLimitNotFoundException("Stake limit not found for device " + deviceUuid);
        }

which chatGPT suggested and I have no clue why that solution works yet mine does not when they basically do the same thing.


Solution

  • So just as @Akashtiwari said I wasn't mocking two methods I was using in service those being existsById and existsByDevice so adding those two mocks fixed the problem. when(deviceRepository.existsById(device.getId())).thenReturn(true); and when(stakeLimitRepository.existsByDevice(device)).thenReturn(true); fixed the issue.