Flow Test 해보기

HEETAE HEO·2022년 9월 19일
0
post-thumbnail

이번 글에서는 Mockito 라이브러리를 사용하여 Text 코드를 작성해보도록 하겠습니다.

Mockito 란??

Mockito는 자바에서 가장 널리 사용되는 목(Mock) 객체 라이브러리 중 하나입니다. 목 객체는 실제 객체를 대신하여 사용될 수 있으며, 테스트 중에 객체 간 상호 작용을 검증하는데 사용됩니다.

Mockito는 객체의 메서드 호출 및 매개변수 등을 쉽게 검증하고, 예상된 결과 값을 반환하도록 설정할 수 있습니다. 또한, Mockito는 안드로이드에서도 사용 가능하며, 안드로이드에서는 Mockito를 사용하여 단위 테스트와 통합 테스트를 모두 수행할 수 있습니다.

Mockito의 주요 기능은 다음과 같습니다.

  • Mock: 객체 생성
  • Stubbing: Mock 객체의 메서드 호출에 대한 반환 값을 지정
  • 검증: Mock 객체의 메서드 호출이 기대한 대로 발생했는지 검증
  • Argument Capturing: Mock 객체에 전달된 인자 값을 캡쳐하여 검증
  • Spy: 실제 객체의 일부를 Mocking하여 검증
  • Annotations : @Mock, @InjectMocks 등의 어노테이션 사용해 객체 생성 및 의존성 주입

간략하게 Mock에 대해서 설명을 해봤고 이제 사용하는 방법에 대해서 설명해드리겠습니다. 버튼을 누르면 숫자를 1씩 증가하는 로직을 먼저 구현하겠습니다.

간단 예시

// viewModel

class MainViewModel: viewModel() {
	private val _number = MutableStateFlow(0)
    val number = _number.asLiveData()
    
    fun increment(){
    	_number.value++
    }


// Activity

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        
     
        viewModel.number.observe(this) { myInt ->
             binding.textView.text = number.toString()
        }
        
        binding.btn.setOnClickListener {
            viewModel.increment()
        }
    }
}

위의 코드에서 Mockito를 사용하여 테스트 코드를 작성해보겠습니다.

class MainViewModelTest {
	
    @Mock
    lateinit var observer: Observer<Int>

    private lateinit var viewModel: MainViewModel

    @Before
    fun setUp() {
        MockitoAnnotations.openMocks(this)
        viewModel = MainViewModel()
    }

    
      @Test
    fun `test increment`() {
        val expectedValue = 1
        `when`(observer.onChanged(expectedValue)).thenAnswer {}
        viewModel.myInt.observeForever(observer)
        viewModel.increment()
        assertEquals(expectedValue, viewModel.myInt.value)
    }

코드를 설명드리기 전에 사용된 어노테이션에 대해서 알려드리겠습니다.

@Mock : Mockito에서 Mock 객체를 만들기 위해 사용되는 어노테이션 입니다. 테스트에서 사용되는 실제 객체를 대신하여 Mock 객체를 사용하면, 테스트를 더 쉽게 작성할 수 있습니다.
@Before: JUnit에서 테스트 실행 전에 메서드에 붙이는 어노테이션 입니다. 보통 테스트에 필요한 데이터나 객체를 초기화 하는 코드에 작성합니다.
@Test: JUnit에서 테스트 메서드에 붙이는 어노테이션입니다. 테스트를 실행하는 코드를 작성합니다.

이제 바로 코드에 대해서 설명하겠습니다. @Mock 어노테이션을 사용해서 observer 객체를 만들고 MockitoAnnotations.openMocks(this)를 사용하여 객체를 초기화합니다. 그런 다음 increment() 메서드를 호출하여 MyViewModel 클래스의 테스트를 수행하고 예상되는 값과 같은지 검증합니다.

이번에는 계산기 로직을 만들고 해당 결과 값을 테스트하는 코드를 작성해보겠습니다.

계산기 테스트 코드

class Calculator @Inject constructor() {

    fun add(a: Int, b: Int): Flow<Int> {
        return flow {
            delay(1000) // 동작한다라는 의미의 1초 지연
            emit(a + b)
        }
    }
}

다음으로 viewModel을 구현하겠습니다.

@HiltViewModel
class MainViewModel @Inject constructor(
	private val calculator: Calculator
) : ViewModel() {

    private val _result = MutableLiveData<Result<Int>>()
    val result: LiveData<Result<Int>> = _result

    fun add(a: Int, b: Int) {
        viewModelScope.launch {
            try {
                _result.value = Result.loading()
                val sum = calculator.add(a, b).first()
                _result.value = Result.success(sum)
            } catch (e: Exception) {
                _result.value = Result.error(e.message)
            }
        }
    }
}

이제 해당 부분을 테스트하는 코드를 작성하겠습니다.

@ExperimentalCoroutinesApi
class CalculatorViewModelTest {

    private lateinit var mainViewModel: MainViewModel
    private lateinit var calculator: Calculator

    private val testDispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(testDispatcher)

    @Before
    fun setup() {
        calculator = mockk()
        mainViewModel = MainViewModel(calculator)
    }

    @Test
    fun SuccessAdd() = testScope.runBlockingTest {
        // given
        val a = 10
        val b = 20
        val sum = 30
        val flow = flowOf(sum)

        coEvery { calculator.add(a, b) } returns flow // calculator.add() 함수를 호출하면 flow를 반환하도록 설정

        mainViewModel.add(a, b)

        val result = mainViewModel.result.getOrAwaitValue()
        assertThat(result.status).isEqualTo(Status.SUCCESS)
        assertThat(result.data).isEqualTo(sum)
    }

    @Test
    fun FailAdd() = testScope.runBlockingTest {
        // given
        val a = 10
        val b = 20
        val exception = Exception("Error")
        val flow = flow<Int> { throw exception }

        coEvery { calculator.add(a, b) } returns flow // calculator.add() 함수를 호출하면 예외를 throw하도록 설정

        mainViewModel.add(a, b)

        val result = mainViewModel.result.getOrAwaitValue()
        assertThat(result.status).isEqualTo(Status.ERROR)
        assertThat(result.error).isEqualTo(exception.message)
    }
}

이제부터 위의 코드를 설명해가겠습니다.

우선 TestCoroutineDispatcher와 TestCoroutineScope에 대해서 설명하겠습니다. 테스트 코루틴을 생성하고, 실행을 제어하는데 사용됩니다. 이를 사용하여 ViewModel에서 호출하는 add() 메서드의 Flow를 제어하고, 테스트에서 결과를 얻을 수 있습니다.

다음은 mockk() 입니다. 해당 함수를 사용하여 Calculator 클래스의 Mock 객체를 만들었고, coEvery { calculator.add(a,b) } return flow 코드를 사용하여 Calculator 클래스의 add() 메서드를 호출하면 Mock 객체가 반환하는 flow를 사용하도록 설정했습니다.

coEvery {} 는 MockK 라이브러리에서 제공하는 함수 중 하나로 코루틴에서 사용하는 함수의 Mock을 생성할 때 사용합니다. coEvery 함수는 suspend 함수를 Mock으로 만들기 위해 사용되며, 생성한 Mock 함수를 원하는 반환 값으로 설정할 수 있습니다. 아래에 예시를 작성 하도록 하겠습니다.

SuccessAdd()는 add() 함수 호출 결과가 성공적으로 ViewModel의 LiveData에 전달되는지 확인하고, FailAdd() 테스트는 add() 함수 호출 결과가 예외적으로 ViewModel의 LiveData에 전달되는지 확인합니다.

이렇게 Mock 객체를 사용하여 Flow를 테스트하는 방법을 살펴보았습니다. Mock 객체를 사용하여 테스트를 하게 되면 의존성을 가진 클래스의 메서드를 직접 호출하지 않고도 테스트할 수 있어 유용합니다.

예시

coEvery { someSuspendFunction() } returns someResult

위의 코드에서 coEvery { someSuspendFunction() } returns someResult는 someSuspendFunction() 함수를 Mock으로 만들고, 이 함수가 호출되면 someResult 를 반환하도록 설정한 것 입니다.

또한 throws 키워드를 사용하여 예외를 던지도록 설정할 수 있습니다. 바로 코드로 보겠습니다.

coEvery { someSuspendFunction() } throws someException

위의 코드를 보면 coEvery { someSuspendFunction() } throws someException 는 someSuspendFunction() 함수를 호출하면 someException을 던지도록 설정한 것 입니다.

Mock 함수가 호출될 때 어떤 매개변수를 받는지 확인하기 위해 coEvery 함수의 인자로 람다 함수를 전달할 수도 있습니다.

coEvery { someSuspendFunction(any()) } returns someResult

someSuspendFunction(any()) } returns someResult는 someSuspendFunction() 함수를 Mock으로 만들고, 이 함수가 호출될 때 어떤 인자든 받아들이도록 하며, 호출되면 someResult를 반환하도록 설정한 것입니다.

이러한 방법으로 coEvery 함수를 사용하여 Mock 함수를 생성하고, 이를 사용하여테스트를 진행할 수 있습니다.

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글