Opacity

Step 1 - difficult to maintain but working code

The main idea of our sample program is to raise and lower widget opacity in cycles. In first attempt "time running" logic is embedded into widget making difficult to write consistent predictable test.

enum AnimationOrder { UP, DOWN }


void go() {
   Timer.periodic(Duration(milliseconds: 50), _progressAnimation());
}

//this is part of a widget so "time iterating" logic is highly coupled with widget/ui layer. 
//This combination is very difficult for testing because it depends on time - which we can not 
//control during tests
  _progressAnimation() => (Timer _) {
        switch (_animationOrder) {
          case AnimationOrder.UP:
            _progress++;
            if (_progress >= 100) _animationOrder = AnimationOrder.DOWN;
            break;
          case AnimationOrder.DOWN:
            _progress--;
            if (_progress <= 0) _animationOrder = AnimationOrder.UP;
            break;
        }
        print("_progress : $_progress");
        setState(() {});
      };


 final _animation = Center(
      child: Opacity(
        opacity: (_progress.toDouble() / 100.0),
        child: Text("some text"),
      ),
    );

Step 2 - extract class responsible for cyclic time measurement

Here at least we extracted logic from the widget but the problem of testing is not solved. Widget has direct dependency on CyclicTimer and CyclicTimer has dependency on non-deterministic timer.

enum _CycleOrder { UP, DOWN }

class CyclicTimer extends ChangeNotifier{

  final int _limit;
  var _currentValue=1;
  var _cycleOrder = _CycleOrder.UP;

  CyclicTimer([this._limit=1]);

  start(){
    Timer.periodic(Duration(milliseconds: 50), _runCycle);
  }

  value() => _currentValue;

  _runCycle(Timer _){
    switch (_cycleOrder) {
      case _CycleOrder.UP:
        _currentValue++;
        if (_currentValue >= _limit) _cycleOrder = _CycleOrder.DOWN;
        break;
      case _CycleOrder.DOWN:
        _currentValue--;
        if (_currentValue <= 0) _cycleOrder = _CycleOrder.UP;
        break;
    }

    notifyListeners();
  }
}

Step 3 - make widget test deterministic

We need to decouple widget from system time based timer implementation

abstract class CyclicTimer extends ChangeNotifier{
  void start();
  int value();
}

class PeriodicCyclicTimer extends CyclicTimer{
...
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState(PeriodicCyclicTimer());
}

Now we can inject our own implementation in tests

class TimerForTests extends CyclicTimer {
  var v = 0;

  @override
  void start() {}

  @override
  int value() => v;

  void changeCounter(int newValue) {
    v = newValue;
    notifyListeners();
  }
}

And to have full control over a widget creation we will us it in our custom structure

class _TestApp extends StatelessWidget {
  final TimerForTests timer;

  const _TestApp(this.timer);

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: HomePage(timer),
      );
}

And Finally our tests where we are testing if our widgets reacts to timer changes

  Opacity _opacityWidget(){
    var opacityFinder = find.byType(Opacity);
    return opacityFinder.evaluate().first.widget as Opacity;
  }

  testWidgets('timer based opacity change', (WidgetTester tester) async {
    final timer = TimerForTests(); //our controlled deterministic timer
    await tester.pumpWidget(_TestApp(timer));

    expect(_opacityWidget().opacity, 0.1);

    //start animation
    await tester.tap(find.byKey(Key("AnimationButton")));
    await tester.pump();

    //lets check if opacity is rising correctly
    timer.changeCounter(50);
    await tester.pump();
    expect(_opacityWidget().opacity, 0.5);

    //and now if it is correctly substracted
    timer.changeCounter(20);
    await tester.pump();
    expect(_opacityWidget().opacity, 0.2);
  });

Step 4 - make widget test deterministic

To achieve this we need to "abstract away" usage of system timer inside our class. We did it by creating our own representation of time scheduler which can be easily mocked in tests - SimpleRunner

mixin SimpleRunner{
  void run(VoidCallback logic);
}

class TimerSimpleRunner with SimpleRunner{

  @override
  void run(VoidCallback logic) {
    Timer.periodic(Duration(milliseconds: 50),(Timer _ ) =>  logic());
  }
}

So now we can use interface directly in our timer class.

class PeriodicCyclicTimer extends CyclicTimer{

  final int _limit;
  final SimpleRunner _runner;
  var _currentValue=1;
  var _cycleOrder = _CycleOrder.UP;

  PeriodicCyclicTimer(this._limit, this._runner);

  @override start(){
    _runner.run(runCycle);
  }

  @override value() => _currentValue;

  runCycle(){
    switch (_cycleOrder) {
      case _CycleOrder.UP:
        _currentValue++;
        if (_currentValue >= _limit) _cycleOrder = _CycleOrder.DOWN;
        break;
      case _CycleOrder.DOWN:
        _currentValue--;
        if (_currentValue <= 0) _cycleOrder = _CycleOrder.UP;
        break;
    }

    notifyListeners();
  }
}

results matching ""

    No results matching ""