How to Unit-Test Thread which takes time to perfor

2019-07-12 18:31发布

问题:

I have Thread which runs while the program runs and polls a queue and check whether it has object and if yes then it calls method on the object

Here is the code :

while(isRunning){
        synchronized (loginQueue) {
            if(loginQueue.peek() != null) {
                Object[] loginObjectWithConnection = loginQueue.poll();
                tryLogin(loginObjectWithConnection);
            }
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
}

Here is the tryLogin method

private void tryLogin(Object[] loginObjectWithConnection) {
        LoginPacket packet = (LoginPacket)loginObjectWithConnection[0];
        Connection connection = (Connection)loginObjectWithConnection[1];

        try {
            if(playerDataService.arevalidCredentials(packet.getUserName(), packet.getPassword())) {

                if(!playerDataService.isPlayerBanned(packet.getUserName())){ //Player exists in the system

                    communicationService.sendTCP(connection, packetFactory.makeLoginFailurePacket(StringConstants.PLAYER_BANNED));

                } else{ //Player is not banned

                }
            } else { // Player does not exist
                communicationService.sendTCP(connection, packetFactory.makeLoginFailurePacket(StringConstants.INVALID_USER));
            }
        } catch (SQLException e) {
            communicationService.sendTCP(connection, packetFactory.makeLoginFailurePacket(StringConstants.SERVER_ERROR));
            e.printStackTrace();
        }
}

Now my problem is that I want to test the invocations of these service methods but when I run the unit tests they won't work as it takes time to reach the point of tryLogin and till then JUnit fails. I tries using Thread.sleep() but I know it is not the right way to do it as it fails sometimes and pass sometimes.

Here is what I have in my Unit Test

@Test
public void theExpectedMessageShouldBeSentIfUserIsBanned() throws InterruptedException, SQLException {
    //Arrange
    when(moqLoginQueue.peek()).thenReturn(object);
    when(moqLoginQueue.poll()).thenReturn(object);
    LoginFailurePacket packet = new LoginFailurePacket(StringConstants.PLAYER_BANNED);
    when(moqPacketFactory.makeLoginFailurePacket(StringConstants.PLAYER_BANNED)).thenReturn(packet);
    when(moqPlayerDataService.arevalidCredentials(anyString(), anyString())).thenReturn(true);
    when(moqPlayerDataService.isPlayerBanned(anyString())).thenReturn(true);

    //Act
    loginManager.start();
    Thread.sleep(10); //Dirty hack -.-

    //Assert
    verify(moqCommunicationService).sendTCP(any(Connection.class), eq(packet));
}

回答1:

The system is untestable in the current form: among good test qualities there are:

  • easy for other programmers to undertsand
  • hard for other programmers to break
  • fast to run

The piece of logic you want to test is LoginManager.tryLogin, which is private in your snippet. If you want to publicly document it (tests are kind of documentation: they state how the system should behave), it has to be public.

I suggest to move all that logic to a method in a new class: Authentication.attempt() (I suggest an immutable object and a method that does not take any argument - someone says the optimal number of arguments in OO design is zero).

Now that testing is feasible, I also think you should get rid of all that code in LoginManager.start(): simply use an ExecutorService and submit authentication attempts - this way you'll have a faster program and less code to test, because the hard (and tricky) part is managed by Java.