Explorar el Código

feat(test): Set up initial load testing infrastructure

Relates to #74
Michael Bromley hace 6 años
padre
commit
2a0b8940d7

+ 6 - 0
packages/dev-server/README.md

@@ -23,3 +23,9 @@ Specify the database as above to populate that database:
 ```bash
 yarn populate --db=sqlite
 ```
+
+## Load testing
+
+This package also contains scripts for load testing the Vendure server. The load testing infrastructure and scripts are located in the [./load-testing](./load-testing) directory.
+
+Load testing is done with [k6](https://docs.k6.io/), and to run them you will need k6 installed and (in Windows) available in your PATH environment variable so that it can be run with the command `k6`.

+ 4 - 0
packages/dev-server/load-testing/graphql/admin/.graphqlconfig

@@ -0,0 +1,4 @@
+{
+  "name": "Load testing admin schema",
+  "schemaPath": "../../../../../schema-admin.json"
+}

+ 10 - 0
packages/dev-server/load-testing/graphql/admin/test.graphql

@@ -0,0 +1,10 @@
+query {
+    activeChannel {
+        code
+    }
+    administrators {
+        items {
+            createdAt
+        }
+    }
+}

+ 4 - 0
packages/dev-server/load-testing/graphql/shop/.graphqlconfig

@@ -0,0 +1,4 @@
+{
+  "name": "Load testing shop schema",
+  "schemaPath": "../../../../../schema-shop.json"
+}

+ 6 - 0
packages/dev-server/load-testing/graphql/shop/add-to-order.graphql

@@ -0,0 +1,6 @@
+mutation ($id: ID! $qty: Int!) {
+  addItemToOrder(productVariantId: $id quantity: $qty) {
+    id
+    code
+  }
+}

+ 99 - 0
packages/dev-server/load-testing/graphql/shop/product.graphql

@@ -0,0 +1,99 @@
+query ($id: ID!){
+  product(id: $id) {
+    ...ProductWithVariants
+  }
+}
+
+fragment ProductWithVariants on Product {
+  id
+  languageCode
+  name
+  slug
+  description
+  featuredAsset {
+    ...Asset
+  }
+  assets {
+    ...Asset
+  }
+  translations {
+    languageCode
+    name
+    slug
+    description
+  }
+  optionGroups {
+    id
+    languageCode
+    code
+    name
+  }
+  variants {
+    ...ProductVariant
+  }
+  facetValues {
+    id
+    code
+    name
+    facet {
+      id
+      name
+    }
+  }
+}
+
+fragment Asset on Asset {
+  id
+  name
+  fileSize
+  mimeType
+  type
+  preview
+  source
+}
+
+fragment ProductVariant on ProductVariant {
+  id
+  languageCode
+  name
+  price
+  currencyCode
+  priceIncludesTax
+  priceWithTax
+  taxRateApplied {
+    id
+    name
+    value
+  }
+  taxCategory {
+    id
+    name
+  }
+  sku
+  options {
+    id
+    code
+    languageCode
+    name
+  }
+  facetValues {
+    id
+    code
+    name
+    facet {
+      id
+      name
+    }
+  }
+  featuredAsset {
+    ...Asset
+  }
+  assets {
+    ...Asset
+  }
+  translations {
+    id
+    languageCode
+    name
+  }
+}

+ 37 - 0
packages/dev-server/load-testing/graphql/shop/search.graphql

@@ -0,0 +1,37 @@
+query {
+  search(input: {
+    take: 20
+    groupByProduct: true
+  }) {
+    items {
+      productId
+      productVariantId
+      productName
+      productVariantName
+      productPreview
+      productVariantPreview
+      price {
+        ... on PriceRange {
+          min
+          max
+        }
+        ... on SinglePrice {
+          value
+        }
+      }
+      score
+    }
+    totalItems
+    facetValues {
+      count
+      facetValue {
+        id
+        name
+        facet {
+          id
+          name
+        }
+      }
+    }
+  }
+}

+ 43 - 0
packages/dev-server/load-testing/scripts/search-and-checkout.js

@@ -0,0 +1,43 @@
+// @ts-check
+import {sleep} from 'k6';
+import {ShopApiRequest} from '../utils/api-request.js';
+
+const searchQuery = new ShopApiRequest('shop/search.graphql');
+const productQuery = new ShopApiRequest('shop/product.graphql');
+const addItemToOrderMutation = new ShopApiRequest('shop/add-to-order.graphql');
+
+/**
+ * Searches for products, adds to order, checks out.
+ */
+export default function() {
+  const itemsToAdd = Math.ceil(Math.random() * 10);
+
+  for (let i = 0; i < itemsToAdd; i ++) {
+    searchProducts();
+    const product = findAndLoadProduct();
+    addToCart(randomItem(product.variants).id);
+  }
+}
+
+function searchProducts() {
+  for (let i = 0; i < 4; i++) {
+    searchQuery.post();
+    sleep(Math.random() * 3 + 0.5);
+  }
+}
+
+function findAndLoadProduct() {
+  const searchResult = searchQuery.post();
+  const items = searchResult.data.search.items;
+  const productResult = productQuery.post({ id: randomItem(items).productId });
+  return productResult.data.product;
+}
+
+function addToCart(variantId) {
+  const qty = Math.ceil(Math.random() * 4);
+  addItemToOrderMutation.post({ id: variantId, qty });
+}
+
+function randomItem(items) {
+  return items[Math.floor(Math.random() * items.length)];
+}

+ 4 - 0
packages/dev-server/load-testing/typings.d.ts

@@ -0,0 +1,4 @@
+/* tslint:disable:no-namespace no-internal-module */
+declare module k6 {
+
+}

+ 24 - 0
packages/dev-server/load-testing/utils/api-request.js

@@ -0,0 +1,24 @@
+// @ts-check
+import http from 'k6/http';
+import { check, fail } from 'k6';
+
+export class ShopApiRequest {
+    constructor(fileName) {
+        this.document = open('../graphql/' + fileName);
+    }
+
+    post(variables = {}) {
+        const res = http.post('http://localhost:3000/shop-api/', {
+            query: this.document,
+            variables: JSON.stringify(variables),
+        });
+        check(res, {
+            'Did not error': r => r.json().errors == null && r.status === 200,
+        });
+        const result = res.json();
+        if (result.errors) {
+            fail('Errored: ' + result.errors[0].message);
+        }
+        return res.json();
+    }
+}

+ 2 - 1
packages/dev-server/package.json

@@ -6,7 +6,8 @@
   "private": true,
   "scripts": {
     "populate": "node -r ts-node/register populate-dev-server.ts",
-    "start": "nodemon --config nodemon-debug.json index.ts"
+    "start": "nodemon --config nodemon-debug.json index.ts",
+    "load-test": "k6 run load-testing/scripts/search-and-checkout.js"
   },
   "dependencies": {
     "@vendure/common": "~0.1.0",