<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Grid Smarter Cities</title>
    <description>The latest articles on DEV Community by Grid Smarter Cities (@gridsmartercities).</description>
    <link>https://dev.to/gridsmartercities</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1007%2F9958e12a-46b0-4419-8dda-cc067e55a9e3.png</url>
      <title>DEV Community: Grid Smarter Cities</title>
      <link>https://dev.to/gridsmartercities</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gridsmartercities"/>
    <language>en</language>
    <item>
      <title>Automating the Deployment of CloudWatch Canaries with CloudFormation</title>
      <dc:creator>James Harrington</dc:creator>
      <pubDate>Wed, 30 Jun 2021 15:04:12 +0000</pubDate>
      <link>https://dev.to/gridsmartercities/automating-the-deployment-of-cloudwatch-canaries-with-cloudformation-1746</link>
      <guid>https://dev.to/gridsmartercities/automating-the-deployment-of-cloudwatch-canaries-with-cloudformation-1746</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html"&gt;CloudWatch Canaries&lt;/a&gt; can monitor your sites and apis, while &lt;a href="https://aws.amazon.com/cloudformation/"&gt;CloudFormation&lt;/a&gt; automates the deployment of AWS resources. In this post, I will show you how these two technologies can work together to automate the deployment of these canaries to monitor a range of applications. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why CloudFormation?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://github.com/gridsmartercities"&gt;Grid Smarter Cities&lt;/a&gt; we use CloudFormation to automate the deployment of a wide range of stacks, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  CI/CD processes for our Front End and Back End applications&lt;/li&gt;
&lt;li&gt;  Management of users and permissions&lt;/li&gt;
&lt;li&gt;  Deployment of tools like CloudWatch Canaries and Lambda Performance Tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We do this because it makes the process of automatically deploying resources efficient and less time consuming. CloudFormation treats infrastructure as code, meaning both resources and their dependencies can be created, validated and deployed from a single template file. &lt;/p&gt;

&lt;p&gt;If the stack needs to be updated, all the user has to do is make the necessary changes to the template, reupload it, and let CloudFormation verify and make all the necessary changes to your resources. If a stack needs duplicating, upload the template again and adjust the parameters and redeploy. If the stack is no longer needed, everything can be deleted with a single button click. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Resources Required Separate To the Stack&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before the stack is deployed, we will need to have a prepared script for the canary and two S3 Buckets. The first bucket is used by the Canary to store its artifacts saved, while it  is running - this could be screenshots, logs or generated reports. The second bucket will be used to store the source code used by the canary - we are using a Node script for this canary. The code for this can be found &lt;a href="https://github.com/gridsmartercities/cloudwatch-canary-template/blob/main/cw-canary/nodejs/node_modules/canary-function.js"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Uploading the Files&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;These files need to be in a particular folder structure - this wasn't clear on the AWS CloudFormation Documentation. However, I eventually found that your canary's code file structure should be /nodejs/node_modules/ .&lt;/p&gt;

&lt;p&gt;The following two commands will create this folder structure, and prepare the zip folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp canary-function.js ./nodejs/node_modules/
zip -r sourcecode.zip ./nodejs/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new sourcecode.zip file just needs to be uploaded directly into the S3 bucket you are using to store the Canary source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Writing The Template&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In this template, we have added parameters for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The name of the Service we're monitoring&lt;/li&gt;
&lt;li&gt;  The name of the Canary&lt;/li&gt;
&lt;li&gt;  The name of Source Code Bucket&lt;/li&gt;
&lt;li&gt;  The name of the object containing the code inside the SourceCode bucket&lt;/li&gt;
&lt;li&gt;  The email address for alert emails to be sent to if the canary fails &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these values will be inputted by the user and passed as parameters when the user has uploaded the template. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;MetaData&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: 'Service'
        Parameters:
          - ServiceName
          - CanaryName
          - AlertEmail
      - Label:
          default: 'Buckets'
        Parameters:
          - CanarySourceCodeBucketName
          - CanarySourceCodeKey
          - CanaryArtifactBucketName
    ParameterLabels:
      ServiceName:
        default: Service Name
      CanaryName:
        default: Canary Name
      AlertEmail:
        default: Email
      CanarySourceCodeBucketName:
        default: Source code bucket name
      CanarySourceCodeKey:
        default: Key for the object 
      CanaryArtifactBucketName:
        default: The name of the S3 bucket where the artefacts will be store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Parameters&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parameters:
  ServiceName:
    Description: Enter a lower case, high level service name without environment details. Used to autofill service names. For example, your-service-name
    Type: String
  CanaryName:
    Description: Enter a lower case name for the canary, as they should be known. For example dave not Dave
    Type: String
  AlertEmail:
    Description: Email address to send staging build alerts to, or example you@example.com
    Type: String
  CanarySourceCodeBucketName:
    Description: S3 Bucket Name where the source code lives
    Type: String
  CanarySourceCodeKey:
    Description: Key of the object which contains the source code
    Type: String
  CanaryArtifactBucketName:
    Description: S3 Bucket Name where the canary saves the screenshots and other 
    Type: String
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The resources we need are: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A CloudWatch Canary&lt;/li&gt;
&lt;li&gt;  A Role for the CloudWatch Canary&lt;/li&gt;
&lt;li&gt;  A policy for the CloudWatch Canary's Role&lt;/li&gt;
&lt;li&gt;  A CloudWatch alert to detect when the Canary fails&lt;/li&gt;
&lt;li&gt;  An SNS topic which will send the email alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;CloudWatch Canary&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The first resource is of type AWS::Synthetics::Canary. The property specifying the canary's name and relevant S3 buckets have been read from the parameters defined earlier in the template. For now, the runtime version, handler and schedule have been hard coded, but these could easily be extracted as parameters to make the template more flexible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudwatchCanary: 
    Type: AWS::Synthetics::Canary
    Properties: 
        ArtifactS3Location: !Sub s3://${CanaryArtifactBucketName}
        Code: 
            Handler: canary-function.handler
            S3Bucket: !Sub ${CanarySourceCodeBucketName}
            S3Key:  !Sub ${CanarySourceCodeKey}
        Name: dave-the-canary
        RuntimeVersion: syn-nodejs-puppeteer-3.1
        Schedule: 
            Expression: rate(5 minutes)
        StartCanaryAfterCreation: true
        ExecutionRoleArn: !GetAtt CloudwatchCanaryRole.Arn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;CloudWatch Canary Policy and CloudWatch Canary Role&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The second and third resources are of type AWS::IAM::Policy and AWS::IAM::Policy. We chose to give the CloudWatch Canary a policy which had been modified from &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Roles.html"&gt;CloudWatchSyntheticsFullAccess&lt;/a&gt;. This gave our canary permission to: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Read data from the source code buckets&lt;/li&gt;
&lt;li&gt;  Save and retrieve data from the artefact bucket&lt;/li&gt;
&lt;li&gt;  Trigger alarms when the canary detects an error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We had to add a few extra actions and modify the resource selectors to match our resource names. There were also some unnecessary actions which - for best practices and security - were removed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;CloudWatch Canary Policy&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudwatchCanaryPolicy:
  Type: AWS::IAM::Policy
  Properties:
    PolicyName: !Sub ${ServiceName}-dave-the-canary-policy
    PolicyDocument:
    Version: '2012-10-17'
    Statement:
    - Effect: Allow
        Action:
        - synthetics:*
        Resource: "*"
    - Effect: Allow
        Action:
        - s3:CreateBucket
        - s3:PutEncryptionConfiguration
        Resource:
        - arn:aws:s3:::*
    - Effect: Allow
        Action:
        - iam:ListRoles
        - s3:ListAllMyBuckets
        - s3:GetBucketLocation
        - xray:GetTraceSummaries
        - xray:BatchGetTraces
        - apigateway:GET
        Resource: "*"
    - Effect: Allow
        Action:
        - s3:GetObject
        - s3:ListBucket
        - s3:PutObject
        Resource: arn:aws:s3:::*
    - Effect: Allow
        Action:
        - s3:GetObjectVersion
        Resource: arn:aws:s3:::*
    - Effect: Allow
        Action:
        - iam:PassRole
        Resource:
        - !Sub arn:aws:iam::*:role/service-role/${CloudwatchCanaryRole}
        Condition:
        StringEquals:
            iam:PassedToService:
            - lambda.amazonaws.com
            - synthetics.amazonaws.com
    - Effect: Allow
        Action:
        - iam:GetRole
        Resource:
        - !Sub arn:aws:iam::*:role/service-role/*
    - Effect: Allow
        Action:
        - cloudwatch:GetMetricData
        - cloudwatch:GetMetricStatistics
        Resource: "*"
    - Effect: Allow
        Action:
        - cloudwatch:PutMetricAlarm
        - cloudwatch:PutMetricData
        - cloudwatch:DeleteAlarms
        Resource:
        - '*'
    - Effect: Allow
        Action:
        - cloudwatch:DescribeAlarms
        Resource:
        - arn:aws:cloudwatch:*:*:alarm:*
    - Effect: Allow
        Action:
        - lambda:CreateFunction
        - lambda:AddPermission
        - lambda:PublishVersion
        - lambda:UpdateFunctionConfiguration
        - lambda:GetFunctionConfiguration
        Resource:
        - arn:aws:lambda:*:*:function:cwsyn-*
    - Effect: Allow
        Action:
        - lambda:GetLayerVersion
        - lambda:PublishLayerVersion
        Resource:
        - arn:aws:lambda:*:*:layer:cwsyn-*
        - arn:aws:lambda:*:*:layer:Synthetics:*
    - Effect: Allow
        Action:
        - ec2:DescribeVpcs
        - ec2:DescribeSubnets
        - ec2:DescribeSecurityGroups
        Resource:
        - "*"
    - Effect: Allow
        Action:
        - sns:ListTopics
        Resource:
        - "*"
    - Effect: Allow
        Action:
        - sns:CreateTopic
        - sns:Subscribe
        - sns:ListSubscriptionsByTopic
        Resource:
        - arn:*:sns:*:*:Synthetics-*
    Roles:
    - !Ref CloudwatchCanaryRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;CloudWatch Canary Role&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudwatchCanaryRole:
    Type: AWS::IAM::Role
    Properties:
        RoleName:  !Sub ${ServiceName}-${CanaryName}-the-canary-role
        AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
            - Effect: Allow
            Principal:
                Service:
                - lambda.amazonaws.com
            Action:
                - sts:AssumeRole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On its own, the deployed canary will monitor resources as canary's source code files. Any issues found by the canary will be visible on the relevant  dashboard in CloudWatch. Unfortunately, this will not trigger any email alerts or notifications. Adding this functionality to our stack is straightforward, we need to add a CloudWatchAlarm and an SNS Topic. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Adding the Topic and Alarm&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;By adding the following sections, alert emails will be sent to the specified email address when submitting the template. &lt;/p&gt;

&lt;p&gt;The CloudWatchCanaryAlarm is triggered when the average success count falls below 100% for one evaluation period; this period was hard coded to five minutes. If this failure is detected, the CloudWatchCanaryAlarmTopic is triggered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudwatchCanaryAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
        ActionsEnabled: true
        AlarmDescription: !Sub ${ServiceName}-${CanaryName}-the-canary-alert
        ComparisonOperator:  LessThanThreshold
        EvaluationPeriods: 1
        DatapointsToAlarm: 1
        MetricName: SuccessPercent
        Namespace: CloudWatchSynthetics
        Period: 60
        Statistic: Average
        Threshold: 100
        TreatMissingData: notBreaching
        AlarmActions:
        - !Ref CloudwatchCanaryAlarmTopic
        Dimensions:
            - Name: CanaryName
            Value: !Sub ${CanaryName}-the-canary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CloudWatchCanaryAlarmTopic sends a message to the AlertEmail address, specified by the CloudFormation parameters inputted by the user when deploying the stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudwatchCanaryAlarmTopic:
    Type: AWS::SNS::Topic
    Properties:
        DisplayName: !Sub ${CanaryName}-cw-canary-alarms
        Subscription:
            - Endpoint: !Ref AlertEmail
            Protocol: email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Deploying The Stack&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;All that's left to do is deploy Dave the Canary. To do this,  upload the template, fill in the form defining the stack parameters as shown below, click next(we'll not configure any of the default stack options), click next again, confirm that you "acknowledge that AWS CloudFormation might create IAM resources with custom names" then click the Create Stack button. Your resources should then be deployed and running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6wvZk2v9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgyqwa0swoocv9c7yeqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6wvZk2v9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgyqwa0swoocv9c7yeqt.png" alt="Screenshot of completed form to set CloudFormation parameters"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Closing Notes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This CloudFormation template was written to help developers at Grid Smarter Cities deploy tools which monitor our products. Deploying a canary from the console is straightforward but using CloudFormation allows us to automate the deployment process, allowing us to set up monitoring tools and alerts for all of our products. &lt;/p&gt;

&lt;p&gt;Writing this template may have required a bit of research and some trial and error but, in doing so, our developers and QAs will never have to manually check that our products are live. If the products aren't, the CloudWatch canary will automatically inform us quickly and more reliably than if we were manually checking ourselves.&lt;/p&gt;

&lt;p&gt;Full copies of both templates (with and without the alerts) and example canary function can be found in the &lt;a href="https://github.com/gridsmartercities/cloudwatch-canary-template"&gt;Repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudformation</category>
      <category>devops</category>
      <category>cloudwatch</category>
    </item>
    <item>
      <title>Integration testing websockets with Python</title>
      <dc:creator>Rob Anderson</dc:creator>
      <pubDate>Mon, 05 Aug 2019 09:21:03 +0000</pubDate>
      <link>https://dev.to/gridsmartercities/integration-testing-websockets-with-python-40jc</link>
      <guid>https://dev.to/gridsmartercities/integration-testing-websockets-with-python-40jc</guid>
      <description>&lt;p&gt;TL;DR - Testing is important but can be difficult. I've written the Python library pywsitest to help with integration testing websockets and included some examples.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://github.com/gridsmartercities" rel="noopener noreferrer"&gt;Grid Smarter Cities&lt;/a&gt; we believe that testing is a key component of good software. &lt;/p&gt;

&lt;p&gt;Our attitude towards testing is reflected primarily in our development process where we practice test driven development, perform code analysis using &lt;a href="https://prospector.readthedocs.io/en/master/" rel="noopener noreferrer"&gt;Prospector&lt;/a&gt;, and require 100% test coverage (which we check using &lt;a href="https://coverage.readthedocs.io/en/v4.5.x/" rel="noopener noreferrer"&gt;Coverage.py&lt;/a&gt;). This process is re-iterated in our automated build pipelines, which run linting, the full suite of unit tests against our code, and integration tests against a staging environment.&lt;/p&gt;

&lt;p&gt;A lot of our ideas about testing are roughly based on a testing pyramid (see below), where because unit tests are fast to write and fast to run, they make up the majority of tests we use. Integration tests take longer to run, generally incur some cost running against a live system, and are more complicated to write; so we write less of them, but they're still an important part of ensuring we have a good product.&lt;/p&gt;

&lt;p&gt;UI tests and manual testing are both more time consuming and more expensive processes, so they make up a smaller portion of our tests. Because they're not something I have to worry too much about as a back-end developer I'm going to skip over them a bit today, though I appreciate the importance of a full testing suite and process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://speakerdeck.com/slobodan/designing-testable-serverless-apps-using-hexagonal-architecture?slide=56" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fspeakerd.s3.amazonaws.com%2Fpresentations%2F6a3d5d3a838e4dcca5c0f4926b3788f4%2Fslide_55.jpg%3F11817400" alt="Test pyramid"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However many unit tests we write, and how much we can trust our code in isolation, we can't have full confidence in our products unless we also test interactions between modules on a live system. Automated integration testing of REST APIs is a reasonably straight forward process using tools like &lt;a href="https://dredd.org/en/latest/" rel="noopener noreferrer"&gt;Dredd&lt;/a&gt; alongside OpenAPI templates, but Grid ran into difficulties testing some of our more complicated interactions with a websocket based API.&lt;/p&gt;

&lt;h2&gt;
  
  
  pywsitest
&lt;/h2&gt;

&lt;p&gt;One of our products that has been developed recently at Grid required different users interacting with and updating a request object in real time. We chose to use a websocket-based API for all of the asynchronous parts of our application.&lt;/p&gt;

&lt;p&gt;When each user type connects to the websocket host, they receive a set of messages about their current request status, other active users, and some geolocation-based data. When one user modifies a request object, certain other users receive the status update, and users are restricted as to which actions they can perfom on a request.&lt;/p&gt;

&lt;p&gt;Testing asynchronous interations between the users became a challenge, and is what lead us to creating our Python library &lt;a href="https://github.com/gridsmartercities/pywsitest" rel="noopener noreferrer"&gt;pywsitest&lt;/a&gt; (python websocket integration testing framework).&lt;/p&gt;

&lt;p&gt;The pywsitest library allows a user to connect to a websocket host, assert that a series of messages have been received, and that any messages that need to be sent were sent successfully. Messages can either be sent on connection, or be triggered by receiving any specified message. This ability to listen for responses, and send messages out when they're received allows for writing tests that target the interaction between different users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing scenario
&lt;/h2&gt;

&lt;p&gt;I've written a websocket based chat server that allows a user to connect using a username, send messages to all connected users through the server, and disconnect from the server when they're finished.&lt;/p&gt;

&lt;p&gt;The user's name will be included in the url as a query parameter. Since I'm running this server locally for testing, the url I'll use to connect will be &lt;code&gt;wss://localhost?name=Rob&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a user connects to the server, the server will broadcast a message to all connected users to tell them the user has connected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rob has connected"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user sends a message to the server, it will be broadcast to all connected users with the format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rob: Hello, world!"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user disconnects, the server will broadcast to all connected users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rob has disconnected"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For my first test I want to ensure a user can connect to the websocket host, and assert that they receive a broadcasted message saying that they've connected.&lt;/p&gt;

&lt;p&gt;To do this I set up a &lt;code&gt;WSTest&lt;/code&gt; object pointing at the correct url, I then added my name as a query parameter. Because we are expecting a message from the host letting us know we've successfully connected, I added an expected response using the &lt;code&gt;with_response&lt;/code&gt; method on the &lt;code&gt;WSTest&lt;/code&gt; instance. Passed into the &lt;code&gt;with_response&lt;/code&gt; method is an instance of &lt;code&gt;WSResponse&lt;/code&gt; with a single attribute matching with the json message I'm expecting to receive from the host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pywsitest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WSResponse&lt;/span&gt;

&lt;span class="n"&gt;ws_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rob has connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws_test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ws_test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the &lt;code&gt;run&lt;/code&gt; method is asynchronous I'm running it synchronously using the &lt;a href="https://docs.python.org/3/library/asyncio.html" rel="noopener noreferrer"&gt;&lt;code&gt;asyncio&lt;/code&gt;&lt;/a&gt; library in this example with the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws_test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next functionality I want to test is the user's abililty to send a message after connecting to the websocket host. This test is set-up in a very similar way to the previous test, but with a message triggered when the connected message is received. The test runner will also expect a response from the host displaying the message that the test runner sent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pywsitest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WSMessage&lt;/span&gt;

&lt;span class="n"&gt;ws_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rob has connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, world!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rob: Hello, world!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws_test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ws_test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last functionality I want to demonstrate will be testing two clients running simultaneously. To do this I'll create methods to run each of the tests, and use &lt;code&gt;asyncio.gather()&lt;/code&gt; to run both of the methods simultaneously. I'll also add a short delay in one of the methods to help ensure that both users have connected before one of them broadcasts a message.&lt;/p&gt;

&lt;p&gt;In this test, &lt;code&gt;user_1&lt;/code&gt; will connect to the websocket host, and wait until it receives a broadcasted message that &lt;code&gt;user_2&lt;/code&gt; has connected before sending a message. &lt;code&gt;user_2&lt;/code&gt; will then reply to that message, at which point &lt;code&gt;user_1&lt;/code&gt;'s test will finish and &lt;code&gt;user_2&lt;/code&gt; will listen for the broadcasted message notifying everyone that &lt;code&gt;user_1&lt;/code&gt; has disconnected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pywsitest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WSMessage&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user_1&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ws_test_1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_1 has connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_2 has connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;WSMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from user_1!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_2: Hello from user_2!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws_test_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ws_test_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user_2&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ws_test_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;WSTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_2 has connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_1: Hello from user_1!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;WSMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from user_2!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WSResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_1 has disconnected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws_test_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ws_test_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_complete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;test_user_1&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;test_user_2&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing notes
&lt;/h2&gt;

&lt;p&gt;pywsitest was written to help enable Grid Smarter Cities to better test our products, and simplifies the process of writing integration tests targeting a websocket-based service.&lt;/p&gt;

&lt;p&gt;It's an open source project, so if you want to get involved in future development it'd be greatly appreciated!&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>websockets</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
