Il s'agit d'une question générale concernant les boulons et les becs de test d'unité dans une topologie Storm écrite en Java.
Quelles sont les pratiques et directives recommandées pour les tests unitaires (JUnit?) Boulons et Becs?
Par exemple, je pourrais écrire un test JUnit pour un Bolt
, mais sans bien comprendre le cadre (comme le cycle de vie d'un Bolt
) et les implications de la sérialisation, faites facilement l'erreur de Constructor-based création de variables membres non sérialisables. Dans JUnit, ce test réussirait, mais dans une topologie, cela ne fonctionnerait pas. J'imagine pleinement qu'il y a de nombreux points de test à considérer (comme cet exemple avec la sérialisation et le cycle de vie).
Par conséquent, il est recommandé que si vous utilisez des tests unitaires basés sur JUnit, vous exécutez une petite topologie fictive (LocalMode
?) Et testez le contrat implicite pour le Bolt
(ou Spout
) sous cette topologie? Ou bien, est-ce OK d'utiliser JUnit, mais l'implication étant que nous devons simuler le cycle de vie d'un Bolt (le créer, appeler prepare()
, se moquer d'un Config
, etc.) soigneusement? Dans ce cas, quels sont les points de test généraux pour la classe testée (Bolt/Spout) à considérer?
Qu'ont fait les autres développeurs en ce qui concerne la création de tests unitaires appropriés?
J'ai remarqué qu'il existe une API de test de topologie (Voir: https://github.com/xumingming/storm-lib/blob/master/src/jvm/storm/TestingApiDemo.Java ). Est-il préférable d'utiliser une partie de cette API et de mettre en place des "topologies de test" pour chaque individu Bolt
& Spout
(et de vérifier le contrat implicite que le Bolt doit fournir, par exemple - c'est Sorties déclarées)?
Merci
Notre approche consiste à utiliser l'injection par le constructeur d'une usine sérialisable dans le bec/boulon. Le bec/boulon consulte alors l'usine dans sa méthode d'ouverture/préparation. La seule responsabilité de l'usine est d'encapsuler l'obtention des dépendances du bec/boulon de manière sérialisable. Cette conception permet à nos tests unitaires d'injecter de fausses usines de test/simulation qui, consultées, renvoient des services simulés. De cette façon, nous pouvons tester de manière étroite le bec/boulons à l'aide de simulateurs, par exemple Mockito.
Voici un exemple générique d'un boulon et un test pour celui-ci. J'ai omis l'implémentation de la fabrique UserNotificationFactory
car cela dépend de votre application. Vous pouvez utiliser des localisateurs de services pour obtenir les services, la configuration sérialisée, la configuration accessible par HDFS, ou vraiment n'importe quel moyen pour obtenir les services corrects, tant que l'usine peut le faire après un cycle de serde. Vous devez couvrir la sérialisation de cette classe.
Boulon
public class NotifyUserBolt extends BaseBasicBolt {
public static final String NAME = "NotifyUser";
private static final String USER_ID_FIELD_NAME = "userId";
private final UserNotifierFactory factory;
transient private UserNotifier notifier;
public NotifyUserBolt(UserNotifierFactory factory) {
checkNotNull(factory);
this.factory = factory;
}
@Override
public void prepare(Map stormConf, TopologyContext context) {
notifier = factory.createUserNotifier();
}
@Override
public void execute(Tuple input, BasicOutputCollector collector) {
// This check ensures that the time-dependency imposed by Storm has been observed
checkState(notifier != null, "Unable to execute because user notifier is unavailable. Was this bolt successfully prepared?");
long userId = input.getLongByField(PreviousBolt.USER_ID_FIELD_NAME);
notifier.notifyUser(userId);
collector.emit(new Values(userId));
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields(USER_ID_FIELD_NAME));
}
}
test
public class NotifyUserBoltTest {
private NotifyUserBolt bolt;
@Mock
private TopologyContext topologyContext;
@Mock
private UserNotifier notifier;
// This test implementation allows us to get the mock to the unit-under-test.
private class TestFactory implements UserNotifierFactory {
private final UserNotifier notifier;
private TestFactory(UserNotifier notifier) {
this.notifier = notifier;
}
@Override
public UserNotifier createUserNotifier() {
return notifier;
}
}
@Before
public void before() {
MockitoAnnotations.initMocks(this);
// The factory will return our mock `notifier`
bolt = new NotifyUserBolt(new TestFactory(notifier));
// Now the bolt is holding on to our mock and is under our control!
bolt.prepare(new Config(), topologyContext);
}
@Test
public void testExecute() {
long userId = 24;
Tuple tuple = mock(Tuple.class);
when(Tuple.getLongByField(PreviousBolt.USER_ID_FIELD_NAME)).thenReturn(userId);
BasicOutputCollector collector = mock(BasicOutputCollector.class);
bolt.execute(Tuple, collector);
// Here we just verify a call on `notifier`, but we could have stubbed out behavior befor
// the call to execute, too.
verify(notifier).notifyUser(userId);
verify(collector).emit(new Values(userId));
}
}
Depuis la version 0.8.1, les installations de tests unitaires de Storm ont été exposées via Java:
Pour un exemple comment utiliser cette API, regardez ici:
Une approche que nous avons adoptée consiste à déplacer la majeure partie de la logique d'application hors des boulons et des becs vers les objets que nous utilisons pour faire le gros du travail en les instanciant et en les utilisant via des interfaces minimales. Ensuite, nous faisons des tests unitaires sur ces objets et des tests d'intégration, bien que cela laisse une lacune.
Il s'avère assez facile de se moquer des objets Storm comme OutputDeclarer, Tuple et OutputFieldsDeclarer. Parmi ceux-ci, seul OutputDeclarer voit des effets secondaires, alors codez la classe factice OutputDeclarer pour pouvoir répondre aux tuples et aux ancres émises, par exemple. Votre classe de test peut ensuite utiliser des instances de ces classes fictives pour configurer facilement une instance de boulon/bec, l'invoquer et valider les effets secondaires attendus.