Hello, Freakin world!

[채팅앱][클라이언트] ClientChannelManager 단위 테스트 작성하기 본문

Toy Project/채팅 앱 만들기

[채팅앱][클라이언트] ClientChannelManager 단위 테스트 작성하기

johnna_endure 2020. 2. 26. 03:17

다시 ClientChannelManager 클래스로 돌아와서 작성했던 기능들의 단위테스트를 작성해보자. 

 

아래는 ClientChannelManager 클래스의 코드다. 

 

import ...

/*
이 클래스의 책임은 어디까지인가?
단순하게 채널 객체의 생성과 반환만을 책임진다.
 */
public class ClientChannelManager {
    private SocketChannel socketChannel;
    private Logger logger = Logger.getLogger(ClientChannelManager.class.getCanonicalName());
    private SocketAddress serverAddress;

    public ClientChannelManager(){}

    public synchronized SocketChannel getChannelInstance(){
        if(socketChannel == null || !socketChannel.isConnected() ||
            !socketChannel.isOpen()) {
            open();
            boolean isConnected = connect(serverAddress);
            if (isConnected) {
                return socketChannel;
            }
        }
        return socketChannel;
    }

    private void open() {
        try {
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            logger.info("[클라이언트] 채널 open 실패 : " + e.getMessage());
        }
    }

    private boolean connect(SocketAddress address){
        try {
            socketChannel.connect(address);
            return socketChannel.finishConnect();
        } catch (IOException e) {
            e.printStackTrace();
            logger.info("[클라이언트] 서버와 connect 실패 : " + e.getMessage());
            return false;
        } catch (ConnectionPendingException e) {
            e.printStackTrace();
            logger.info("[클라이언트] 잘못된 서버 address 입니다.");
            return false;
        }
    }

    public void setServerAddress(SocketAddress serverAddress) {
        this.serverAddress = serverAddress;
    }
}

 

모든 메서드에 대해서 단위 테스트를 할 필요는 없다. 

위 클래스 중 private 접근자를 가진 메서드는 단순히 자바 api를 감싼 메서드일 뿐이다. 

굳이 신뢰성이 보장된 라이브러리의 테스트까지 하는 건 시간낭비가 아닐까?

 

(하지만 private 메서드라도 종종 테스트가 필요한 메서드가 생길 수도 있는데. 그럴 땐 자바의 reflection api를 사용해 테스트를 작성할 수 있다.)

 

결국 테스트가 필요한 메서드는 getChannelInstance() 메서드이다. 

이 메서드를 테스트하기 위해서는 Mock 서버가 필요하다. 

 

아래는 간단하게 클라이언트의 연결 요청만 받아주는 MockServer 클래스다.

 

import ...

public class MockServer extends Thread{
    public Selector selector;
    public ServerSocketChannel serverSocketChannel;
    SocketAddress address;
    public boolean isContinue = true;

    public MockServer(SocketAddress address) {
        this.address = address;
    }

	public void init()  {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(address);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(isContinue) {
            try {
                selector.select();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            selectionKeys.stream()
                    .filter(SelectionKey::isAcceptable)
                    .forEach(this::acceptHandler);
        }
    }

    private void acceptHandler(SelectionKey key) {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        try {
            serverSocketChannel.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

이 클래스의 중요한 포인트는 Junit 테스트 중 비동기로 실행되어 각 테스트 메서드들의 연결 요청을 처리할 수 있어야 한다는 것이다. 

 

처음에는 내부에 main 메서드를 만들어 테스트 시작 전에 실행시켜 두었는데, 진짜 너무 구린 방식이다.

이런 방식으로는 자동화된 테스트들을 만들 수 없다. 

 

그래서 Junit 애너테이션 정보들을 찾아보니 @BeforeClass 라는 쓸만한 녀석을 발견했다.

이 애너테이션을 public static void 메서드 위에 달아놓으면 그 메서드는 모든 테스트 메서드 실행 전, 딱 한번 실행된다. (@AfterClass도 동일한 개념이다.)

 

이를 바탕으로 작성한 테스트 메서드다.

import ...

public class TestClientChannelManager {
    ClientChannelManager manager;
    SocketAddress fakeAddress;
    SocketAddress correctAddress;
    static MockServer mockServer;

    @BeforeClass
    public static void beforeClass() {
        System.out.println("Mock 서버 시작");
        mockServer = new MockServer(new InetSocketAddress("localhost", 9090));
        mockServer.init();
        mockServer.start();
    }

    @Before
    public void before() {
        manager = new ClientChannelManager();
        fakeAddress = new InetSocketAddress("localhost", 9005);
        correctAddress  = new InetSocketAddress("localhost", 9090);
    }

    @AfterClass
    public static void afterClass() throws IOException {
        mockServer.serverSocketChannel.close();
        mockServer.isContinue = false;

        mockServer.selector.wakeup();
    }

    /*
    연결에 성공해서 연결된 채널 객체를 반환함.
    반환된 객체가 null 이 아닌지 확인.
     */
    @Test
    public void testGetConnetedChannel_whenSuccess() {
        manager.setServerAddress(correctAddress);
        assertThat(manager.getChannelInstance().isConnected()).isTrue();
    }

    /*
    처음 getConnectedChannelInstance 을 호출해서 반환에 실패했을 때
    null 을 반환하는가?
     */
    @Test
    public void testGetConnetedChannel_whenFirstInitiatingIsFailed(){
        manager.setServerAddress(fakeAddress); //임의의 address 객체
        assertThat(manager.getChannelInstance().isConnected()).isFalse();
    }

    /*
    연결된 채널을 한번 close 한 후 다시
    잘못된 주소로 생성하는 시도에서 실패할 경우 연결되지 않은 채널 객체를 반환하는가?
    어떻게 테스트할 것인가?
     */
    @Test
    public void testGetConnetedChannel_whenSecondlyInitiatingIsFailed() throws IOException {
        manager.setServerAddress(correctAddress);
        SocketChannel channel = manager.getChannelInstance();
        channel.close();

        manager.setServerAddress(fakeAddress);
        channel = manager.getChannelInstance();

        assertThat(channel.isConnected()).isFalse();
    }
    /*
        같은 포트에 close 했다가 다시 연결하는 경우.
    */
    @Test
    public void testGetConnetedChannel_whenSecondlyInitiatingIsSuccessful() throws IOException {
        manager.setServerAddress(correctAddress);
        SocketChannel channel = manager.getChannelInstance();
        channel.close();
        channel = manager.getChannelInstance();

        assertThat(channel.isConnected()).isTrue();
    }
}

 

테스트 성공!!!

 

 

 

Comments