Ceci est le guide du AWS CDK développeur de la version 2. L'ancienne CDK version 1 est entrée en maintenance le 1er juin 2022 et a pris fin le 1er juin 2023.
Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.
Avec le AWS CDK, votre infrastructure peut être aussi testable que n'importe quel autre code que vous écrivez. L'approche standard pour tester AWS CDK les applications utilise le module AWS CDK d'assertions et des frameworks de test populaires tels que Jest
Il existe deux catégories de tests que vous pouvez écrire pour les AWS CDK applications.
-
Des assertions détaillées testent des aspects spécifiques du AWS CloudFormation modèle généré, tels que « cette ressource possède cette propriété avec cette valeur ». Ces tests permettent de détecter des régressions. Ils sont également utiles lorsque vous développez de nouvelles fonctionnalités à l'aide du développement piloté par les tests. (Vous pouvez d'abord écrire un test, puis le réussir en écrivant une implémentation correcte.) Les assertions précises sont les tests les plus fréquemment utilisés.
-
Les tests instantanés testent le AWS CloudFormation modèle synthétisé par rapport à un modèle de référence précédemment stocké. Les tests instantanés vous permettent de refactoriser librement, car vous pouvez être sûr que le code refactorisé fonctionne exactement de la même manière que le code original. Si les modifications étaient intentionnelles, vous pouvez accepter une nouvelle référence pour les futurs tests. Toutefois, les CDK mises à niveau peuvent également entraîner la modification des modèles synthétisés. Vous ne pouvez donc pas vous fier uniquement aux instantanés pour vous assurer que votre implémentation est correcte.
Note
Les versions complètes TypeScript des applications Python et Java utilisées comme exemples dans cette rubrique sont disponibles sur GitHub
Premiers pas
Pour illustrer comment écrire ces tests, nous allons créer une pile contenant une machine à AWS Step Functions états et une AWS Lambda fonction. La fonction Lambda est abonnée à une SNS rubrique Amazon et transmet simplement le message à la machine à états.
Tout d'abord, créez un projet d'CDKapplication vide à l'aide du CDK Toolkit et en installant les bibliothèques dont nous aurons besoin. Les constructions que nous utiliserons se trouvent toutes dans le CDK package principal, qui est une dépendance par défaut dans les projets créés avec le CDK Toolkit. Cependant, vous devez installer votre framework de test.
mkdir state-machine && cd state-machine cdk init --language=typescript npm install --save-dev jest @types/jest
Créez un répertoire pour vos tests.
mkdir test
Modifiez les projets package.json
pour indiquer NPM comment exécuter Jest et pour indiquer à Jest quels types de fichiers collecter. Les modifications nécessaires sont les suivantes.
-
Ajouter une nouvelle
test
clé à lascripts
section -
Ajoutez Jest et ses types à la section
devDependencies
-
Ajouter une nouvelle clé
jest
de niveau supérieur avec une déclarationmoduleFileExtensions
Ces modifications sont présentées dans le schéma suivant. Placez le nouveau texte à l'endroit indiqué danspackage.json
. Les espaces réservés «... » indiquent les parties existantes du fichier qui ne doivent pas être modifiées.
{
...
"scripts": {
...
"test": "jest"
},
"devDependencies": {
...
"@types/jest": "^24.0.18",
"jest": "^24.9.0"
},
"jest": {
"moduleFileExtensions": ["js"]
}
}
La pile d'exemples
Voici la pile qui sera testée dans cette rubrique. Comme nous l'avons décrit précédemment, il contient une fonction Lambda et une machine à états Step Functions, et accepte une ou plusieurs rubriques AmazonSNS. La fonction Lambda est abonnée aux SNS rubriques Amazon et les transmet à la machine à états.
Vous n'avez rien à faire de spécial pour rendre l'application testable. En fait, cette CDK pile n'est pas différente des autres exemples de piles présentés dans ce guide.
Entrez le code suivant dans lib/state-machine-stack.ts
:
import * as cdk from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import * as sns_subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as sfn from "aws-cdk-lib/aws-stepfunctions";
import { Construct } from "constructs";
export interface StateMachineStackProps extends cdk.StackProps {
readonly topics: sns.Topic[];
}
export class StateMachineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: StateMachineStackProps) {
super(scope, id, props);
// In the future this state machine will do some work...
const stateMachine = new sfn.StateMachine(this, "StateMachine", {
definition: new sfn.Pass(this, "StartState"),
});
// This Lambda function starts the state machine.
const func = new lambda.Function(this, "LambdaFunction", {
runtime: lambda.Runtime.NODEJS_18_X,
handler: "handler",
code: lambda.Code.fromAsset("./start-state-machine"),
environment: {
STATE_MACHINE_ARN: stateMachine.stateMachineArn,
},
});
stateMachine.grantStartExecution(func);
const subscription = new sns_subscriptions.LambdaSubscription(func);
for (const topic of props.topics) {
topic.addSubscription(subscription);
}
}
}
Nous allons modifier le point d'entrée principal de l'application afin de ne pas réellement instancier notre stack. Nous ne voulons pas le déployer accidentellement. Nos tests créeront une application et une instance de la pile à des fins de test. Cette tactique est utile lorsqu'elle est associée au développement piloté par les tests : assurez-vous que la pile passe tous les tests avant d'activer le déploiement.
Dans bin/state-machine.ts
:
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
const app = new cdk.App();
// Stacks are intentionally not created here -- this application isn't meant to
// be deployed.
La fonction Lambda
Notre exemple de pile inclut une fonction Lambda qui démarre notre machine à états. Nous devons fournir le code source de cette fonction afin qu'ils CDK puissent la regrouper et la déployer dans le cadre de la création de la ressource de fonction Lambda.
-
Créez le dossier
start-state-machine
dans le répertoire principal de l'application. -
Dans ce dossier, créez au moins un fichier. Par exemple, vous pouvez enregistrer le code suivant dans
start-state-machines/index.js
.exports.handler = async function (event, context) { return 'hello world'; };
Cependant, n'importe quel fichier fonctionnera, car nous ne déploierons pas réellement la pile.
Exécution de tests
À titre de référence, voici les commandes que vous utilisez pour exécuter des tests dans votre AWS CDK application. Il s'agit des mêmes commandes que vous utiliseriez pour exécuter les tests dans n'importe quel projet utilisant le même framework de test. Pour les langages qui nécessitent une étape de compilation, incluez-la pour vous assurer que vos tests ont été compilés.
tsc && npm test
Assertions fines
La première étape pour tester une pile avec des assertions détaillées consiste à synthétiser la pile, car nous écrivons des assertions par rapport au modèle généré. AWS CloudFormation
Nous StateMachineStackStack
exigeons que nous lui transmettions le SNS sujet Amazon pour qu'il soit transféré à la machine d'état. Dans notre test, nous allons donc créer une pile séparée pour contenir le sujet.
Normalement, lorsque vous écrivez une CDK application, vous pouvez sous-classer Stack
et instancier le sujet SNS Amazon dans le constructeur de la pile. Dans notre test, nous instancions Stack
directement, puis nous transmettons cette pile comme scope, en Topic
l'attachant à la pile. C'est équivalent sur le plan fonctionnel et moins verbeux. Cela permet également de rendre les piles utilisées uniquement dans les tests « différentes » des piles que vous avez l'intention de déployer.
import { Capture, Match, Template } from "aws-cdk-lib/assertions";
import * as cdk from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import { StateMachineStack } from "../lib/state-machine-stack";
describe("StateMachineStack", () => {
test("synthesizes the way we expect", () => {
const app = new cdk.App();
// Since the StateMachineStack consumes resources from a separate stack
// (cross-stack references), we create a stack for our SNS topics to live
// in here. These topics can then be passed to the StateMachineStack later,
// creating a cross-stack reference.
const topicsStack = new cdk.Stack(app, "TopicsStack");
// Create the topic the stack we're testing will reference.
const topics = [new sns.Topic(topicsStack, "Topic1", {})];
// Create the StateMachineStack.
const stateMachineStack = new StateMachineStack(app, "StateMachineStack", {
topics: topics, // Cross-stack reference
});
// Prepare the stack for assertions.
const template = Template.fromStack(stateMachineStack);
}
Nous pouvons maintenant affirmer que la fonction Lambda et l'SNSabonnement Amazon ont été créés.
// Assert it creates the function with the correct properties...
template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "handler",
Runtime: "nodejs14.x",
});
// Creates the subscription...
template.resourceCountIs("AWS::SNS::Subscription", 1);
Notre test de fonction Lambda affirme que deux propriétés particulières de la ressource fonctionnelle ont des valeurs spécifiques. Par défaut, la hasResourceProperties
méthode effectue une correspondance partielle sur les propriétés de la ressource telles qu'elles sont indiquées dans le CloudFormation modèle synthétisé. Ce test nécessite que les propriétés fournies existent et aient les valeurs spécifiées, mais la ressource peut également avoir d'autres propriétés, qui ne sont pas testées.
Notre SNS assertion Amazon affirme que le modèle synthétisé contient un abonnement, mais rien sur l'abonnement lui-même. Nous avons inclus cette assertion principalement pour illustrer comment affirmer le nombre de ressources. La Template
classe propose des méthodes plus spécifiques pour écrire des assertions par rapport aux Mapping
sections Resources
Outputs
, et du CloudFormation modèle.
Allumeurs
Le comportement de correspondance partielle par défaut de hasResourceProperties
peut être modifié à l'aide des matchers de la Match
classe.
Les matchers vont de indulgent (Match.anyValue
) à strict (Match.objectEquals
). Ils peuvent être imbriqués pour appliquer différentes méthodes de correspondance aux différentes parties des propriétés des ressources. En utilisant Match.objectEquals
et Match.anyValue
ensemble, par exemple, nous pouvons tester le IAM rôle de la machine à états de manière plus complète, sans avoir besoin de valeurs spécifiques pour les propriétés susceptibles de changer.
// Fully assert on the state machine's IAM role with matchers.
template.hasResourceProperties(
"AWS::IAM::Role",
Match.objectEquals({
AssumeRolePolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: {
"Fn::Join": [
"",
["states.", Match.anyValue(), ".amazonaws.com"],
],
},
},
},
],
},
})
);
De nombreuses CloudFormation ressources incluent des JSON objets sérialisés représentés sous forme de chaînes. Le Match.serializedJson()
matcher peut être utilisé pour faire correspondre les propriétés qu'il contientJSON.
Par exemple, les machines à états Step Functions sont définies à l'aide d'une chaîne JSON basée sur Amazon States Language. Nous allons nous Match.serializedJson()
assurer que notre état initial est la seule étape. Encore une fois, nous utiliserons des matchers imbriqués pour appliquer différents types de correspondance aux différentes parties de l'objet.
// Assert on the state machine's definition with the Match.serializedJson()
// matcher.
template.hasResourceProperties("AWS::StepFunctions::StateMachine", {
DefinitionString: Match.serializedJson(
// Match.objectEquals() is used implicitly, but we use it explicitly
// here for extra clarity.
Match.objectEquals({
StartAt: "StartState",
States: {
StartState: {
Type: "Pass",
End: true,
// Make sure this state doesn't provide a next state -- we can't
// provide both Next and set End to true.
Next: Match.absent(),
},
},
})
),
});
Capture
Il est souvent utile de tester les propriétés pour s'assurer qu'elles suivent des formats spécifiques ou qu'elles ont la même valeur qu'une autre propriété, sans avoir besoin de connaître leurs valeurs exactes à l'avance. Le assertions
module fournit cette fonctionnalité dans sa Capture
classe.
En spécifiant une Capture
instance à la place d'une valeur inhasResourceProperties
, cette valeur est conservée dans l'Capture
objet. La valeur capturée réelle peut être récupérée à l'aide as
des méthodes de l'objet asNumber()
asString()
, notammentasObject
, et soumise à un test. À utiliser Capture
avec un comparateur pour spécifier l'emplacement exact de la valeur à capturer dans les propriétés de la ressource, y compris les propriétés sérialiséesJSON.
L'exemple suivant teste pour s'assurer que l'état de départ de notre machine à états porte un nom commençant parStart
. Il vérifie également que cet état est présent dans la liste des états de la machine.
// Capture some data from the state machine's definition.
const startAtCapture = new Capture();
const statesCapture = new Capture();
template.hasResourceProperties("AWS::StepFunctions::StateMachine", {
DefinitionString: Match.serializedJson(
Match.objectLike({
StartAt: startAtCapture,
States: statesCapture,
})
),
});
// Assert that the start state starts with "Start".
expect(startAtCapture.asString()).toEqual(expect.stringMatching(/^Start/));
// Assert that the start state actually exists in the states object of the
// state machine definition.
expect(statesCapture.asObject()).toHaveProperty(startAtCapture.asString());
Tests instantanés
Lors des tests instantanés, vous comparez l'intégralité du CloudFormation modèle synthétisé à un modèle de référence précédemment stocké (souvent appelé « modèle principal »). Contrairement aux assertions détaillées, les tests instantanés ne sont pas utiles pour détecter les régressions. Cela est dû au fait que les tests instantanés s'appliquent à l'ensemble du modèle et que d'autres éléments que les modifications de code peuvent entraîner de légères (ou not-so-small) différences dans les résultats de synthèse. Ces modifications n'affecteront peut-être même pas votre déploiement, mais elles entraîneront tout de même l'échec d'un test de capture instantanée.
Par exemple, vous pouvez mettre à jour une CDK structure pour y intégrer une nouvelle bonne pratique, ce qui peut entraîner des modifications des ressources synthétisées ou de la façon dont elles sont organisées. Vous pouvez également mettre à jour le CDK kit d'outils vers une version qui indique des métadonnées supplémentaires. Les modifications apportées aux valeurs de contexte peuvent également affecter le modèle synthétisé.
Les tests instantanés peuvent toutefois être d'une grande aide pour le refactoring, à condition de maintenir constants tous les autres facteurs susceptibles d'affecter le modèle synthétisé. Vous saurez immédiatement si une modification que vous avez apportée a involontairement modifié le modèle. Si le changement est intentionnel, il suffit d'accepter le nouveau modèle comme base de référence.
Par exemple, si nous avons cette DeadLetterQueue
construction :
export class DeadLetterQueue extends sqs.Queue {
public readonly messagesInQueueAlarm: cloudwatch.IAlarm;
constructor(scope: Construct, id: string) {
super(scope, id);
// Add the alarm
this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', {
alarmDescription: 'There are messages in the Dead Letter Queue',
evaluationPeriods: 1,
threshold: 1,
metric: this.metricApproximateNumberOfMessagesVisible(),
});
}
}
Nous pouvons le tester comme ceci :
import { Match, Template } from "aws-cdk-lib/assertions";
import * as cdk from "aws-cdk-lib";
import { DeadLetterQueue } from "../lib/dead-letter-queue";
describe("DeadLetterQueue", () => {
test("matches the snapshot", () => {
const stack = new cdk.Stack();
new DeadLetterQueue(stack, "DeadLetterQueue");
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});
});
Conseils pour les tests
N'oubliez pas que vos tests dureront aussi longtemps que le code qu'ils testent, et qu'ils seront lus et modifiés aussi souvent. Par conséquent, il vaut la peine de prendre un moment pour réfléchir à la meilleure façon de les écrire.
Ne copiez pas et ne collez pas de lignes de configuration ou d'assertions courantes. Refactorisez plutôt cette logique en accessoires ou en fonctions auxiliaires. Utilisez de bons noms qui reflètent ce que chaque test teste réellement.
N'essayez pas d'en faire trop en un seul test. De préférence, un test ne doit tester qu'un seul comportement. Si vous rompez accidentellement ce comportement, un seul test doit échouer, et le nom du test doit vous indiquer ce qui a échoué. Cependant, il s'agit plutôt d'un idéal à atteindre ; il arrive que vous écriviez inévitablement (ou par inadvertance) des tests qui testent plus d'un comportement. Pour les raisons que nous avons déjà décrites, les tests instantanés sont particulièrement sujets à ce problème. Utilisez-les donc avec parcimonie.