23

I'm currently developing an app that uses the bloc architecture. My bloc is using streams exclusively to communicate with the UI. Therefore all its methods except for the constructor are private ( they start by '_').

So the question is how can I test the bloc's private methods from the test class that lives in the text package so it cannot access private methods of other packages.

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40
onthemoon
  • 3,302
  • 3
  • 17
  • 24
  • 3
    Test private content by testing public stuff. – Rémi Rousselet Jan 16 '19 at 15:01
  • But a test failing in a public method that calls a lot of private functions is not as useful as a test failing in the exact private function where the problem is. – onthemoon Jan 16 '19 at 15:11
  • 1
    You're free to extract that private logic into another object – Rémi Rousselet Jan 16 '19 at 15:33
  • 1
    @Rémi Rousselet, I think you are right. I am taking your approach as an opportunity to refactor the big class I was using and decompose it in smaller objects , some of which can expose their methods and can therefore be unit tested – onthemoon Jan 16 '19 at 16:12
  • 1
    Anyway, I've found a workaround: for each private function that I want to test ( eg. _foo(), I also define a public function foo(){ _foo(); }. Then I place all the public version of such functions in the same area of the file and can easily comment them out for release code. It would be great to conditionally enable their definitions only for debug builds. Is there any way to do it in dart/ flutter? something like #if def ( DEBUG).... – onthemoon Jan 16 '19 at 16:41

3 Answers3

30

You can't, but you can make them public and annotate it with @visibleForTesting to get an DartAnalyzer warning when they are accessed from code that is not in in the same library or in test/

https://github.com/dart-lang/sdk/blob/master/pkg/meta/lib/meta.dart#L224-L233

/// Used to annotate a declaration was made public, so that it is more visible
/// than otherwise necessary, to make code testable.
///
/// Tools, such as the analyzer, can provide feedback if
///
/// * the annotation is associated with a declaration not in the `lib` folder
///   of a package, or
/// * the declaration is referenced outside of its the defining library or a
///   library which is in the `test` folder of the defining package.
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 2
    That should still be avoided as much as possible – Rémi Rousselet Jan 16 '19 at 15:05
  • 5
    That's a rather philosophical point of view. My answer is about if or how it can be done. The rest is not Dart/Flutter specific. See https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones – Günter Zöchbauer Jan 16 '19 at 15:08
  • the annotation @VisibleForTesting, doesn't seem to be recognized by Flutter – onthemoon Jan 16 '19 at 16:36
  • It should be `@visibleForTesting` (lower-case `v`) or `@VisibleForTesting()` (upper-case `V` and `()` or did you mean that the analyzer doesn't produce warnings? – Günter Zöchbauer Jan 16 '19 at 16:38
  • the analyzer says: "error: Undefined name 'VisibleForTesting' used as an annotation.". The same using lower-case v. – onthemoon Jan 16 '19 at 16:42
  • 2
    You can import it from Flutter (`package:flutter/foundation.dart` exports it.) or add the `meta` package to `dependencies:` in `pubspec.yaml` (and import it from there). – Günter Zöchbauer Jan 16 '19 at 17:03
  • 1
    I found that using the meta package didn't help, but importing foundation.dart did work – Mark Aug 15 '19 at 00:30
5

I solved this problem right now by making another public 'stub' method with the same parameters that just calls the private one and marked it with @visibleForTesting. This

@visibleForTesting
  Future<void> removeAppointments(List<DocumentSnapshot> documents, [FirebaseFirestore instance]) => _removeAppointments(documents, instance);
Arghya Sadhu
  • 41,002
  • 9
  • 78
  • 107
Elisey Ozerov
  • 210
  • 2
  • 7
3

In some cases, another possibility is to separate your class into a public interface and a private implementation. For example, instead of:

class MyClass {
  final String name;

  MyClass(this.name);

  void publicMethod() {
    // ...
  }

  void _privateMethod() {
    // ...
  }
}

split it into:

my_class.dart:

import 'src/my_class_impl.dart';

abstract class MyClass {
  factory MyClass(String name) => MyClassImpl(name);

  void publicMethod();
}

src/my_class_impl.dart:

import 'package:my_package/my_class.dart';

class MyClassImpl implements MyClass {
  final String name;

  MyClassImpl(this.name);

  @override
  void publicMethod() {
    // ...
  }

  void privateMethod() {
    // ...
  }
}

Now your tests can do import 'package:my_package/src/my_class_impl.dart'; and directly access its private methods. Of course, there's nothing stopping someone else from directly importing the implementation library too, but importing files from another package's src/ directory is discouraged. Anyone doing that would be explicitly choosing to undermine encapsulation (as opposed to depending on @visibleForTesting, where breaking encapsulation could more easily be done by accident).

This approach would not be appropriate for all situations. For example, this approach also effectively makes it impossible for others to extend MyClass.

jamesdlin
  • 81,374
  • 13
  • 159
  • 204