dart-lang / mockito

Mockito-inspired mock library for Dart
https://pub.dev/packages/mockito
Apache License 2.0
632 stars 162 forks source link

Support for real executions on Mocking classes #734

Open rafaelsetragni opened 8 months ago

rafaelsetragni commented 8 months ago

Hi everyone,

I am working with Mockito in a Flutter project and facing a challenge in testing classes designed with the Chain of Responsibility pattern. Specifically, I need to partially mock a class where only one real method is executed, and the other related methods are mocked with fake results. After the execution, I need to verify which methods were called or not using verify().called(1) or verifyNever().

Here is a simple example:

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'test.mocks.dart';

enum MathOperation {
  add,
  subtract,
  multiply,
  divide,
}

class Calculator {

  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;
  int multiply(int a, int b) => a * b;
  int divide(int a, int b) => a % b;

  int execute(int a, int b, {required MathOperation operation}){
    late int result;
    switch(operation){

      case MathOperation.add:
        result = add(a, b);
        break;

      case MathOperation.subtract:
        result = subtract(a, b);
        break;

      case MathOperation.multiply:
        result = multiply(a, b);
        break;

      case MathOperation.divide:
        result = divide(a, b);
        break;
    }
    return result;
  }
}

@GenerateNiceMocks([MockSpec<Calculator>()])
void main(){
  final MockCalculator mockedCalculator = MockCalculator();
  setUpAll(() {
    when(mockedCalculator
        .execute(any, any, operation: anyNamed('operation'))
    ).thenExecuteRealMethod();

    when(mockedCalculator.add(any, any)).thenReturn(0);
    when(mockedCalculator.subtract(any, any)).thenReturn(0);
    when(mockedCalculator.multiply(any, any)).thenReturn(0);
    when(mockedCalculator.divide(any, any)).thenReturn(0);
  });

  test('calculation methods tests', () {
    final calculator = Calculator();
    expect(calculator.execute(1, 2, operation: MathOperation.add), 3);
    expect(calculator.execute(1, 2, operation: MathOperation.subtract), -1);
    expect(calculator.execute(1, 2, operation: MathOperation.multiply), 2);
    expect(calculator.execute(1, 2, operation: MathOperation.divide), 1);
  });

  test('calculation exception tests', () {
    final calculator = Calculator();
    expect(() => calculator.execute(0, 0, operation: MathOperation.add), returnsNormally);
    expect(() => calculator.execute(0, 0, operation: MathOperation.subtract), returnsNormally);
    expect(() => calculator.execute(0, 0, operation: MathOperation.multiply), returnsNormally);
    expect(() => calculator.execute(0, 0, operation: MathOperation.divide), throwsA(isA<UnsupportedError>()));
  });

  test('addition call tests', () {
    mockedCalculator.execute(0, 0, operation: MathOperation.add);

    verify(mockedCalculator.add(any, any)).called(1);
    verify(mockedCalculator.subtract(any, any)).called(0);
    verify(mockedCalculator.multiply(any, any)).called(0);
    verify(mockedCalculator.divide(any, any)).called(0);
  });

  test('subtraction call tests', () {
    mockedCalculator.execute(0, 0, operation: MathOperation.add);

    verify(mockedCalculator.add(any, any)).called(0);
    verify(mockedCalculator.subtract(any, any)).called(1);
    verify(mockedCalculator.multiply(any, any)).called(0);
    verify(mockedCalculator.divide(any, any)).called(0);
  });

  test('multiplication call tests', () {
    mockedCalculator.execute(0, 0, operation: MathOperation.add);

    verify(mockedCalculator.add(any, any)).called(0);
    verify(mockedCalculator.subtract(any, any)).called(0);
    verify(mockedCalculator.multiply(any, any)).called(1);
    verify(mockedCalculator.divide(any, any)).called(0);
  });

  test('divide call tests', () {
    mockedCalculator.execute(0, 0, operation: MathOperation.add);

    verify(mockedCalculator.add(any, any)).called(0);
    verify(mockedCalculator.subtract(any, any)).called(0);
    verify(mockedCalculator.multiply(any, any)).called(0);
    verify(mockedCalculator.divide(any, any)).called(1);
  });
}

In the provided example, I need to test the Calculator class by mocking its add, subtract, multiply, and divide methods individually while ensuring the execute method uses the real implementation. This is important because each method should be tested separately, without being affected by the others.

Note that in this example I just called thenExecuteRealMethod because this is what Im trying to achieve, but this method doesn't exist and I don't know any replacement for it.

Current Behavior

Currently, Mockito does not seem to support this use case directly. A potential workaround involves extending the Calculator class and overriding methods to throw exceptions, but this approach does not allow the use of Mockito's features like counting executions or other verifications.

Expected Behavior

I am looking for a solution where I can specify which methods to mock and which to keep real in my tests. This would enable precise control over the behavior of the class under test and allow me to verify method calls accurately.

Possible Solutions or Ideas

Perhaps introducing a feature called thenExecuteRealMethod in Mockito that allows partial mocking or selective real method invocations in a mocked class.

srawlins commented 8 months ago

I believe what you are looking for is a "spy"; execute real implementation code, and gather invocation data (how many calls, what arguments). Mockito does not do spying, and the current implementation does not support this idea, because there is no real instance to call. Here's the feature request for Spy: https://github.com/dart-lang/mockito/issues/575.