Dans ce billet, je m'appuie sur l'approche du billet précédent et je montre comment vous pouvez utiliser TinkerGraph pour tester unitairement vos charges de travail transactionnelles. En outre, je montre comment utiliser TinkerGraph en mode intégré. Le mode intégré nécessite l'utilisation de Java, mais il simplifie considérablement l'environnement de test car il n'est pas nécessaire d'exécuter le serveur en tant que processus distinct.
Les deux diagrammes suivants montrent les différences architecturales entre l'exécution d'une requête sur Neptune et l'exécution d'une requête sur un graphe intégré.
Les exemples présentés dans ce billet supposent que vous travaillez avec Java et que vous avez donc accès à la version intégrée de TinkerGraph. Voir Tests automatisés de l'accès aux données d'Amazon Neptune avec Apache TinkerPop Gremlin pour plus d'informations sur l'utilisation de la version distante de TinkerGraph dans un conteneur Docker. Notez que les transactions intégrées ont plus de capacités que les transactions à distance, vous ne devez donc tester que les fonctionnalités qui existent pour les transactions à distance (ce qui est utilisé lors de la connexion à Neptune).
Aperçu des transactions dans TinkerGraph et Neptune
Historiquement, l'un des inconvénients de l'utilisation de TinkerGraph pour les tests était qu'il ne prenait pas en charge les transactions. Les transactions sont un élément important pour garantir l'exactitude des modifications apportées à la base de données sous-jacente, et ce type de comportement ne pouvait pas être testé avec TinkerGraph. Cependant, avec l l'introduction du TinkerGraph transactionneltransactionnel, TinkerTransactionGraph, dans la version 3.7.0, cela a changé et TinkerGraph est une solution appropriée dans la plupart des cas.
Il existe des différences importantes entre la sémantique des transactions de TinkerTransactionGraph et celle de Neptune, et il y a donc des scénarios que vous ne devriez pas tester avec TinkerTransactionGraph. Ces scénarios doivent être couverts par votre suite de tests complète, qui doit être exécutée avec Neptune.
Tout d'abord, TinkerTransactionGraph ne fournit des garanties que contre les lectures sales, il a donc un niveau d'isolation de lecture engagée. Neptune, quant à lui, peut fournir des garanties solides contre les lectures sales, les lectures fantômes et les lectures non répétables. Cela signifie que vos tests unitaires devraient être écrits en pensant que seules les lectures sales ne peuvent pas se produire.
Deuxièmement, TinkerTransactionGraph utilise une forme de verrouillage optimiste, de sorte que si deux transactions tentent de modifier le même élément, la seconde transaction lèvera une exception. Neptune utilise un verrouillage pessimiste (approche wait-lock) et autorise un temps d'attente maximum pour l'acquisition d'une ressource. Il se peut que vous deviez tenir compte de ce comportement de verrouillage optimiste en attrapant des exceptions de transaction
et en réessayant.
En outre, il existe des différences dans la prise en charge de Gremlin entre TinkerGraph et Neptune. Pour plus d'informations, voir Tests automatisés de l'accès aux données d'Amazon Neptune avec Apache TinkerPop Gremlin et Conformité aux normes Gremlin dans Amazon Neptune.
Exemples de tests unitaires TinkerGraph
Prenons l'exemple d'un simple service d'aéroport.
Conditions préalables
Pour exécuter ces exemples contre le TinkerGraph transactionnel directement, vous devez inclure l'artefact tinkergraph-gremlin dans votre construction. Par exemple, si vous utilisez Maven, vous devez inclure la dépendance suivante dans votre fichier pom :
<dependency>
<groupId>org.apache.tinkerpop</groupId>
<artifactId>tinkergraph-gremlin</artifactId>
<version>3.7.0</version>
<scope>test</scope>
</dependency>
La version 3.7.0 est utilisée ici à titre d'exemple car il s'agit de la première version de TinkerGraph transactionnel. La version que vous devez utiliser dépend de la version de votre moteur Neptune. Voir ce tableau pour plus d'informations.
Pour exécuter ces exemples avec Neptune, vous devez également avoir accès à un cluster Neptune.
Exemple de service d'aéroport
Le code suivant montre à quoi pourrait ressembler l'interface d'un tel service :
public interface AirportService {
public boolean addAirport(Map<String, Object> airportData);
public boolean addRoute(String fromAirport, String toAirport, int distance);
public Map<String, Object> getAirportData(String airportCode);
public int getRouteDistance(String fromAirportCode, String toAirportCode);
public boolean hasRoute(String fromAirport, String toAirport);
public boolean removeAirport(String airportCode);
public boolean removeRoute(String fromAirportCode, String toAirportCode);
}
Voyons maintenant à quoi pourrait ressembler l'implémentation de la fonction addRoute
. Le code suivant montre l'implémentation de la méthode addRoute
et quelques champs de la classe :
public class NorthAmericanAirportService implements AirportService {
private GraphTraversalSource g;
public NorthAmericanAirportService(GraphTraversalSource g) {
this.g = g;
}
/**
* Adds a route between two airports.
*
* @param fromAirportCode The airport code of airport where the route begins.
* @param toAirportCode The airport code of airport where the route ends.
* @param distance The distance between the two airports.
* @return True if the route was added; false otherwise.
*/
public boolean addRoute(String fromAirportCode, String toAirportCode, int distance) {
Transaction tx = g.tx();
GraphTraversalSource gtx = tx.begin(); // Explicitly starting the transaction.
// This try-catch-rollback approach is recommended with TinkerPop transactions.
try {
final Vertex fromV = gtx.V().has("code", fromAirportCode).next();
final Vertex toV = gtx.V().has("code", toAirportCode).next();
gtx.addE("route").from(fromV).to(toV).next();
tx.commit();
return true;
} catch (Exception e) {
tx.rollback();
return false;
}
}
Nous pourrions vouloir avoir deux tests unitaires pour cette méthode : un pour un aéroport inexistant, qui devrait échouer, et un pour les aéroports valides, qui devrait réussir. Remarquez que la variable d'instance g est utilisée pour passer d'un fournisseur de graphe à l'autre :
public class AirportServiceTest {
// In this example, "STAGING_ENV" is used to determine whether to test against TinkerGraph or Amazon Neptune.
private static boolean STAGING_ENV = (null != System.getProperty("STAGING_ENV"));
private static Cluster cluster;
private GraphTraversalSource g;
@BeforeClass
public static void setUpServerCluster() {
if (STAGING_ENV) {
cluster = Cluster.build().addContactPoint("your-neptune-cluster").enableSsl(true).create();
}
}
@Before
public void setUpGraph() {
if (STAGING_ENV) { // In this example, STAGING_ENV is a system property used to determine which database to use.
g = traversal().withRemote(DriverRemoteConnection.using(cluster));
g.V().drop().iterate();
// Currently, Neptune only accepts URLs reachable within its VPC as an input to the io step.
g.io("your-presigned-s3-url").read().iterate();
} else {
// Create a default, empty instance of the transactional TinkerGraph.
g = TinkerTransactionGraph.open().traversal();
g.io("your-local-data.xml").read().iterate(); // This is how you insert your GraphML test data.
g.tx().commit(); // By default, transactions are automatically opened, so commit the changes from io().
}
}
@Test
public void testAddRouteWithIncorrectAirportCode() {
final NorthAmericanAirportService service = new NorthAmericanAirportService(g);
// Add route with airport code that doesn't exist.
final boolean wasAdded = service.addRoute("INCORRECT", "AUS", 500);
assertFalse(wasAdded);
// Check to see if any routes exist between those two airports.
assertEquals(0L,
g.E().where(inV().has("code", "INCORRECT"))
.where(outV().has("code", "AUS")).count().next().longValue());
}
@Test
public void testAddRouteWithValidAirportCodes() {
final NorthAmericanAirportService service = new NorthAmericanAirportService(g);
final boolean wasAdded = service.addRoute("PBI", "ORD", 500);
assertTrue(wasAdded);
assertEquals(1L,
g.E().where(inV().has("code", "PBI"))
.where(outV().has("code", "ORD")).count().next().longValue());
}
Voyons à quoi cela pourrait ressembler dans un scénario un peu plus compliqué, où l'on souhaite interrompre temporairement les itinéraires vers un aéroport spécifique. Le code suivant illustre la mise en œuvre d'une fonction qui arrête le trafic entrant :
/**
* Removes incoming routes to an airport.
*
* @param airportCode The airport code of the airport to remove incoming routes.
*/
public void stopIncomingTraffic(String airportCode) {
Transaction tx = g.tx();
try {
g.V().has("code", airportCode).inE().drop().iterate();
tx.commit();
} catch (Exception e) {
tx.rollback();
}
}
Le code suivant illustre un test unitaire pour stopIncomingTraffic()
:
@Test
public void testStoppingTrafficToAus() {
final NorthAmericanAirportService service = new NorthAmericanAirportService(g);
final String airport = "AUS";
service.stopIncomingTraffic(airport);
// Check that there are no outgoing routes into that airport.
assertEquals(0, g.V().out().has("code", airport).toList().size());
}
Nettoyage
Si vous avez suivi les exemples en utilisant un TinkerGraph intégré, celui-ci sera automatiquement nettoyé à la fin des tests.
Si vous avez suivi les exemples en utilisant un cluster Neptune, vous pouvez éviter les frais en supprimant le cluster Neptune.
Conclusion
Les tests unitaires sont un aspect important de CI/CD. Pour des raisons de coût et de flexibilité, vous pouvez exécuter vos tests unitaires sur TinkerGraph. Le TinkerGraph transactionnel, TinkerTransactionGraph, introduit dans la version 3.7.0, est un bon candidat pour tester les transactions. Pour la partie staging de votre pipeline CI/CD, qui exécute des tests moins fréquents comme la performance ou l'intégration, vous pouvez envisager d'exécuter contre une instance de test de Amazon Neptune Serverlessqui est un moyen rentable d'exécuter des charges de test irrégulières et qui aura la même sémantique de transaction que votre base de données Neptune de production.
"Ce billet est le fruit d'une collaboration entre Improving et AWS et fait l'objet d'une publication croisée sur le blog d'Improving et sur le AWS Database Blog."