diff --git a/services/amo/amo-downloads.tester.js b/services/amo/amo-downloads.tester.js
index 58116c4c4e995b7355dcd71cbe84089ae81228b7..0d48839018a15588fc740973d05cbecd5c4ead6e 100644
--- a/services/amo/amo-downloads.tester.js
+++ b/services/amo/amo-downloads.tester.js
@@ -1,22 +1,17 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetricOverTimePeriod } = require('../test-validators')
 const t = (module.exports = new ServiceTester({ id: 'amo', title: 'AMO' }))
 
 t.create('Weekly Downloads')
   .get('/dw/IndieGala-Helper.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'downloads', value: isMetricOverTimePeriod })
-  )
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('Weekly Downloads (not found)')
   .get('/dw/not-a-real-plugin.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 t.create('/d URL should redirect to /dw')
   .get('/d/IndieGala-Helper.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'downloads', value: isMetricOverTimePeriod })
-  )
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
diff --git a/services/amo/amo-rating.tester.js b/services/amo/amo-rating.tester.js
index f5cf47dc3ab326e42f878cd79d5f4a7f795d831a..f17a90656f67ee0b20d190bbdf340063c3d9d264 100644
--- a/services/amo/amo-rating.tester.js
+++ b/services/amo/amo-rating.tester.js
@@ -6,17 +6,15 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Rating')
   .get('/rating/IndieGala-Helper.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: Joi.string().regex(/^\d\/\d$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: Joi.string().regex(/^\d\/\d$/),
+  })
 
 t.create('Stars')
   .get('/stars/IndieGala-Helper.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'stars', value: isStarRating }))
+  .expectBadge({ label: 'stars', message: isStarRating })
 
 t.create('Rating (not found)')
   .get('/rating/not-a-real-plugin.json')
-  .expectJSON({ name: 'mozilla add-on', value: 'not found' })
+  .expectBadge({ label: 'mozilla add-on', message: 'not found' })
diff --git a/services/amo/amo-users.tester.js b/services/amo/amo-users.tester.js
index 39392c0d6d15f0c45e4dffc2e22af687520e3c9c..a0f168ad0fa37b9c1e4f997e08b1d8d7d6a289a0 100644
--- a/services/amo/amo-users.tester.js
+++ b/services/amo/amo-users.tester.js
@@ -1,13 +1,12 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Users')
   .get('/IndieGala-Helper.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'users', value: isMetric }))
+  .expectBadge({ label: 'users', message: isMetric })
 
 t.create('Users (not found)')
   .get('/not-a-real-plugin.json')
-  .expectJSON({ name: 'users', value: 'not found' })
+  .expectBadge({ label: 'users', message: 'not found' })
diff --git a/services/amo/amo-version.tester.js b/services/amo/amo-version.tester.js
index e85ae20b2db635cad16ad7808646a248a76ac25e..23dac4cb9aee8120ef010873e3f6d0d9cdd56ab3 100644
--- a/services/amo/amo-version.tester.js
+++ b/services/amo/amo-version.tester.js
@@ -1,18 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Version')
   .get('/IndieGala-Helper.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'mozilla add-on',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'mozilla add-on',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Version (not found)')
   .get('/not-a-real-plugin.json')
-  .expectJSON({ name: 'mozilla add-on', value: 'not found' })
+  .expectBadge({ label: 'mozilla add-on', message: 'not found' })
diff --git a/services/ansible/ansible-quality.tester.js b/services/ansible/ansible-quality.tester.js
index e7a812f414fd6e61825781cac70ca1ed806749bb..0db519f25cb844832ec311d826da08c5867617ba 100644
--- a/services/ansible/ansible-quality.tester.js
+++ b/services/ansible/ansible-quality.tester.js
@@ -1,15 +1,12 @@
 'use strict'
 
-const Joi = require('joi')
 const { nonNegativeInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('quality score (valid)')
   .get('/432.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'quality', value: nonNegativeInteger })
-  )
+  .expectBadge({ label: 'quality', message: nonNegativeInteger })
 
 t.create('quality score (not found)')
   .get('/0101.json')
-  .expectJSON({ name: 'quality', value: 'no score available' })
+  .expectBadge({ label: 'quality', message: 'no score available' })
diff --git a/services/ansible/ansible-role.tester.js b/services/ansible/ansible-role.tester.js
index 54e6b929e356e28e1d06073e07bd57289f38ffbd..ac1a3980d4585029a62343c4e88c5cad04fd10b1 100644
--- a/services/ansible/ansible-role.tester.js
+++ b/services/ansible/ansible-role.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric } = require('../test-validators')
 
@@ -12,18 +11,16 @@ const t = (module.exports = new ServiceTester({
 
 t.create('role name (valid)')
   .get('/14542.json')
-  .expectJSON({ name: 'role', value: 'openwisp.openwisp2' })
+  .expectBadge({ label: 'role', message: 'openwisp.openwisp2' })
 
 t.create('role name (not found)')
   .get('/000.json')
-  .expectJSON({ name: 'role', value: 'not found' })
+  .expectBadge({ label: 'role', message: 'not found' })
 
 t.create('role downloads (valid)')
   .get('/d/14542.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'role downloads', value: isMetric })
-  )
+  .expectBadge({ label: 'role downloads', message: isMetric })
 
 t.create('role downloads (not found)')
   .get('/d/does-not-exist.json')
-  .expectJSON({ name: 'role downloads', value: 'not found' })
+  .expectBadge({ label: 'role downloads', message: 'not found' })
diff --git a/services/apm/apm.tester.js b/services/apm/apm.tester.js
index af7aa3828322103ed27fd13b58c8d9c990bdb0c0..8286aa1385f9703c335d649559444285455474fb 100644
--- a/services/apm/apm.tester.js
+++ b/services/apm/apm.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { invalidJSON } = require('../response-fixtures')
 const { isMetric, isVPlusTripleDottedVersion } = require('../test-validators')
@@ -12,29 +11,27 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Downloads')
   .get('/dm/vim-mode.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('Version')
   .get('/v/vim-mode.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'apm', value: isVPlusTripleDottedVersion })
-  )
+  .expectBadge({ label: 'apm', message: isVPlusTripleDottedVersion })
 
 t.create('License')
   .get('/l/vim-mode.json')
-  .expectJSON({ name: 'license', value: 'MIT' })
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 t.create('Downloads | Package not found')
   .get('/dm/notapackage.json')
-  .expectJSON({ name: 'downloads', value: 'package not found' })
+  .expectBadge({ label: 'downloads', message: 'package not found' })
 
 t.create('Version | Package not found')
   .get('/v/notapackage.json')
-  .expectJSON({ name: 'apm', value: 'package not found' })
+  .expectBadge({ label: 'apm', message: 'package not found' })
 
 t.create('License | Package not found')
   .get('/l/notapackage.json')
-  .expectJSON({ name: 'license', value: 'package not found' })
+  .expectBadge({ label: 'license', message: 'package not found' })
 
 t.create('Invalid version')
   .get('/dm/vim-mode.json')
@@ -43,7 +40,7 @@ t.create('Invalid version')
       .get('/api/packages/vim-mode')
       .reply([200, '{"releases":{}}'])
   )
-  .expectJSON({ name: 'downloads', value: 'unparseable json response' })
+  .expectBadge({ label: 'downloads', message: 'unparseable json response' })
 
 t.create('Invalid License')
   .get('/l/vim-mode.json')
@@ -52,7 +49,7 @@ t.create('Invalid License')
       .get('/api/packages/vim-mode')
       .reply([200, '{"metadata":{}}'])
   )
-  .expectJSON({ name: 'license', value: 'unparseable json response' })
+  .expectBadge({ label: 'license', message: 'unparseable json response' })
 
 t.create('Unexpected response')
   .get('/dm/vim-mode.json')
@@ -61,4 +58,4 @@ t.create('Unexpected response')
       .get('/api/packages/vim-mode')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'downloads', value: 'unparseable json response' })
+  .expectBadge({ label: 'downloads', message: 'unparseable json response' })
diff --git a/services/appveyor/appveyor-ci.tester.js b/services/appveyor/appveyor-ci.tester.js
index ae7eaea86faf4dc404150c54a66f9234303ab5d1..ce8be0d3eec404f62e02609fd3c1129f3ce2dabd 100644
--- a/services/appveyor/appveyor-ci.tester.js
+++ b/services/appveyor/appveyor-ci.tester.js
@@ -1,23 +1,25 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('CI status')
   .timeout(10000)
   .get('/gruntjs/grunt.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'build', value: isBuildStatus }))
+  .expectBadge({ label: 'build', message: isBuildStatus })
 
 t.create('CI status on branch')
   .timeout(10000)
   .get('/gruntjs/grunt/master.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'build', value: isBuildStatus }))
+  .expectBadge({ label: 'build', message: isBuildStatus })
 
 t.create('CI status on nonexistent project')
   .timeout(10000)
   .get('/somerandomproject/thatdoesntexist.json')
-  .expectJSON({ name: 'build', value: 'project not found or access denied' })
+  .expectBadge({
+    label: 'build',
+    message: 'project not found or access denied',
+  })
 
 t.create('CI status on project that does exist but has no builds yet')
   .get('/gruntjs/grunt.json?style=_shields_test')
@@ -26,4 +28,8 @@ t.create('CI status on project that does exist but has no builds yet')
       .get('/gruntjs/grunt')
       .reply(200, {})
   )
-  .expectJSON({ name: 'build', value: 'no builds found', color: 'lightgrey' })
+  .expectBadge({
+    label: 'build',
+    message: 'no builds found',
+    color: 'lightgrey',
+  })
diff --git a/services/appveyor/appveyor-tests.tester.js b/services/appveyor/appveyor-tests.tester.js
index 8f67ffe53d19f6efe0252a04f7573c99301d5b5e..490282f38dd737665a9de38f000b87b3363c11e3 100644
--- a/services/appveyor/appveyor-tests.tester.js
+++ b/services/appveyor/appveyor-tests.tester.js
@@ -23,23 +23,17 @@ const isCompactCustomAppveyorTestTotals = Joi.string().regex(
 t.create('Test status')
   .timeout(10000)
   .get('/NZSmartie/coap-net-iu0to.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isAppveyorTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isAppveyorTestTotals })
 
 t.create('Test status on branch')
   .timeout(10000)
   .get('/NZSmartie/coap-net-iu0to/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isAppveyorTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isAppveyorTestTotals })
 
 t.create('Test status with compact message')
   .timeout(10000)
   .get('/NZSmartie/coap-net-iu0to.json?compact_message')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isCompactAppveyorTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isCompactAppveyorTestTotals })
 
 t.create('Test status with custom labels')
   .timeout(10000)
@@ -50,9 +44,7 @@ t.create('Test status with custom labels')
       skipped_label: 'n/a',
     },
   })
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isCustomAppveyorTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isCustomAppveyorTestTotals })
 
 t.create('Test status with compact message and custom labels')
   .timeout(10000)
@@ -64,17 +56,18 @@ t.create('Test status with compact message and custom labels')
       skipped_label: '🤷',
     })}`
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: isCompactCustomAppveyorTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: isCompactCustomAppveyorTestTotals,
+  })
 
 t.create('Test status on non-existent project')
   .timeout(10000)
   .get('/somerandomproject/thatdoesntexist.json')
-  .expectJSON({ name: 'tests', value: 'project not found or access denied' })
+  .expectBadge({
+    label: 'tests',
+    message: 'project not found or access denied',
+  })
 
 t.create('Test status on project that does exist but has no builds yet')
   .get('/gruntjs/grunt.json?style=_shields_test')
@@ -83,4 +76,8 @@ t.create('Test status on project that does exist but has no builds yet')
       .get('/gruntjs/grunt')
       .reply(200, {})
   )
-  .expectJSON({ name: 'tests', value: 'no builds found', color: 'lightgrey' })
+  .expectBadge({
+    label: 'tests',
+    message: 'no builds found',
+    color: 'lightgrey',
+  })
diff --git a/services/aur/aur.tester.js b/services/aur/aur.tester.js
index 00633a9160aa12835874e11b237c7beaa8962ee9..ed76d3f065c25f367455b28b7e5dcd94ac146c26 100644
--- a/services/aur/aur.tester.js
+++ b/services/aur/aur.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const {
   isVPlusDottedVersionNClausesWithOptionalSuffix,
@@ -16,49 +15,43 @@ const t = (module.exports = new ServiceTester({
 
 t.create('version (valid)')
   .get('/version/yaourt.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'aur',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-      color: 'blue',
-    })
-  )
+  .expectBadge({
+    label: 'aur',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+    color: 'blue',
+  })
 
 t.create('version (valid, out of date)')
   .get('/version/gog-gemini-rue.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'aur',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-      color: 'orange',
-    })
-  )
+  .expectBadge({
+    label: 'aur',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+    color: 'orange',
+  })
 
 t.create('version (not found)')
   .get('/version/not-a-package.json')
-  .expectJSON({ name: 'aur', value: 'package not found' })
+  .expectBadge({ label: 'aur', message: 'package not found' })
 
 // votes tests
 
 t.create('votes (valid)')
   .get('/votes/yaourt.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'votes',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'votes',
+    message: isMetric,
+  })
 
 t.create('votes (not found)')
   .get('/votes/not-a-package.json')
-  .expectJSON({ name: 'votes', value: 'package not found' })
+  .expectBadge({ label: 'votes', message: 'package not found' })
 
 // license tests
 
 t.create('license (valid)')
   .get('/license/yaourt.json')
-  .expectJSON({ name: 'license', value: 'GPL' })
+  .expectBadge({ label: 'license', message: 'GPL' })
 
 t.create('license (not found)')
   .get('/license/not-a-package.json')
-  .expectJSON({ name: 'license', value: 'package not found' })
+  .expectBadge({ label: 'license', message: 'package not found' })
diff --git a/services/azure-devops/azure-devops-build.tester.js b/services/azure-devops/azure-devops-build.tester.js
index ff5378a98c2340662926a5c854310c62b7b94a87..a495e213f12d912d24385a33beeb7f54c0ee7491 100644
--- a/services/azure-devops/azure-devops-build.tester.js
+++ b/services/azure-devops/azure-devops-build.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -9,37 +8,33 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('default branch')
   .get('/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/2.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('named branch')
   .get('/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/2/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('unknown definition')
   .get('/larsbrinkhoff/953a34b9-5966-4923-a48a-c41874cfb5f5/515.json')
-  .expectJSON({ name: 'build', value: 'definition not found' })
+  .expectBadge({ label: 'build', message: 'definition not found' })
 
 t.create('unknown project')
   .get('/larsbrinkhoff/foo/515.json')
-  .expectJSON({ name: 'build', value: 'user or project not found' })
+  .expectBadge({ label: 'build', message: 'user or project not found' })
 
 t.create('unknown user')
   .get('/notarealuser/foo/515.json')
-  .expectJSON({ name: 'build', value: 'user or project not found' })
+  .expectBadge({ label: 'build', message: 'user or project not found' })
 
 // The following build definition has always a partially succeeded status
 t.create('partially succeeded build')
   .get(
     '/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/4.json?style=_shields_test'
   )
-  .expectJSON({ name: 'build', value: 'passing', color: 'orange' })
+  .expectBadge({ label: 'build', message: 'passing', color: 'orange' })
diff --git a/services/azure-devops/azure-devops-coverage.tester.js b/services/azure-devops/azure-devops-coverage.tester.js
index 0f75e03f3c182e07f5b2d1bcfeaa109100c176c2..859ac35c4d7a998fbfa8ca6712c42c2d9fb978c7 100644
--- a/services/azure-devops/azure-devops-coverage.tester.js
+++ b/services/azure-devops/azure-devops-coverage.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -56,25 +55,21 @@ const expCoverageMultipleReports = '77%'
 
 t.create('default branch coverage')
   .get(`${uriPrefix}/${linuxDefinitionId}.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('named branch without ref')
   .get(`${uriPrefix}/${windowsDefinitionId}/init.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('unknown build definition')
   .get(`${uriPrefix}/${nonExistentDefinitionId}.json`)
-  .expectJSON({ name: 'coverage', value: 'build pipeline not found' })
+  .expectBadge({ label: 'coverage', message: 'build pipeline not found' })
 
 t.create('404 latest build error response')
   .get(mockBadgeUriPath)
@@ -83,9 +78,9 @@ t.create('404 latest build error response')
       .get(mockLatestBuildApiUriPath)
       .reply(404)
   )
-  .expectJSON({
-    name: 'coverage',
-    value: 'build pipeline or coverage not found',
+  .expectBadge({
+    label: 'coverage',
+    message: 'build pipeline or coverage not found',
   })
 
 t.create('no build response')
@@ -100,7 +95,7 @@ t.create('no build response')
         value: [],
       })
   )
-  .expectJSON({ name: 'coverage', value: 'build pipeline not found' })
+  .expectBadge({ label: 'coverage', message: 'build pipeline not found' })
 
 t.create('404 code coverage error response')
   .get(mockBadgeUriPath)
@@ -111,9 +106,9 @@ t.create('404 code coverage error response')
       .get(mockCodeCoverageApiUriPath)
       .reply(404)
   )
-  .expectJSON({
-    name: 'coverage',
-    value: 'build pipeline or coverage not found',
+  .expectBadge({
+    label: 'coverage',
+    message: 'build pipeline or coverage not found',
   })
 
 t.create('invalid code coverage response')
@@ -125,7 +120,7 @@ t.create('invalid code coverage response')
       .get(mockCodeCoverageApiUriPath)
       .reply(200, {})
   )
-  .expectJSON({ name: 'coverage', value: 'invalid response data' })
+  .expectBadge({ label: 'coverage', message: 'invalid response data' })
 
 t.create('no code coverage reports')
   .get(mockBadgeUriPath)
@@ -136,7 +131,7 @@ t.create('no code coverage reports')
       .get(mockCodeCoverageApiUriPath)
       .reply(200, { coverageData: [] })
   )
-  .expectJSON({ name: 'coverage', value: '0%' })
+  .expectBadge({ label: 'coverage', message: '0%' })
 
 t.create('no code coverage reports')
   .get(mockBadgeUriPath)
@@ -147,7 +142,7 @@ t.create('no code coverage reports')
       .get(mockCodeCoverageApiUriPath)
       .reply(200, { coverageData: [] })
   )
-  .expectJSON({ name: 'coverage', value: '0%' })
+  .expectBadge({ label: 'coverage', message: '0%' })
 
 t.create('no line coverage stats')
   .get(mockBadgeUriPath)
@@ -164,7 +159,7 @@ t.create('no line coverage stats')
         ],
       })
   )
-  .expectJSON({ name: 'coverage', value: '0%' })
+  .expectBadge({ label: 'coverage', message: '0%' })
 
 t.create('single line coverage stats')
   .get(mockBadgeUriPath)
@@ -181,7 +176,7 @@ t.create('single line coverage stats')
         ],
       })
   )
-  .expectJSON({ name: 'coverage', value: expCoverageSingleReport })
+  .expectBadge({ label: 'coverage', message: expCoverageSingleReport })
 
 t.create('mixed line and branch coverage stats')
   .get(mockBadgeUriPath)
@@ -198,7 +193,7 @@ t.create('mixed line and branch coverage stats')
         ],
       })
   )
-  .expectJSON({ name: 'coverage', value: expCoverageSingleReport })
+  .expectBadge({ label: 'coverage', message: expCoverageSingleReport })
 
 t.create('multiple line coverage stat reports')
   .get(mockBadgeUriPath)
@@ -235,7 +230,7 @@ t.create('single JaCoCo style line coverage stats')
         ],
       })
   )
-  .expectJSON({ name: 'coverage', value: expCoverageSingleReport })
+  .expectBadge({ label: 'coverage', message: expCoverageSingleReport })
 
 t.create('mixed JaCoCo style line and branch coverage stats')
   .get(mockBadgeUriPath)
@@ -252,7 +247,7 @@ t.create('mixed JaCoCo style line and branch coverage stats')
         ],
       })
   )
-  .expectJSON({ name: 'coverage', value: expCoverageSingleReport })
+  .expectBadge({ label: 'coverage', message: expCoverageSingleReport })
 
 t.create('multiple JaCoCo style line coverage stat reports')
   .get(mockBadgeUriPath)
@@ -269,4 +264,4 @@ t.create('multiple JaCoCo style line coverage stat reports')
         ],
       })
   )
-  .expectJSON({ name: 'coverage', value: expCoverageMultipleReports })
+  .expectBadge({ label: 'coverage', message: expCoverageMultipleReports })
diff --git a/services/azure-devops/azure-devops-release.tester.js b/services/azure-devops/azure-devops-release.tester.js
index 03817052d07e3586f78d686205bb3a17c1949a05..f3d24e91a26976439087f38cd3d3ad0eab8a58a6 100644
--- a/services/azure-devops/azure-devops-release.tester.js
+++ b/services/azure-devops/azure-devops-release.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -9,28 +8,32 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('release status is succeeded')
   .get('/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/1/1.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'deployment',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'deployment',
+    message: isBuildStatus,
+  })
 
 t.create('unknown environment')
   .get('/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/1/515.json')
-  .expectJSON({ name: 'deployment', value: 'user or environment not found' })
+  .expectBadge({
+    label: 'deployment',
+    message: 'user or environment not found',
+  })
 
 t.create('unknown definition')
   .get('/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/515/515.json')
-  .expectJSON({
-    name: 'deployment',
-    value: 'inaccessible or definition not found',
+  .expectBadge({
+    label: 'deployment',
+    message: 'inaccessible or definition not found',
   })
 
 t.create('unknown project')
   .get('/totodem/515/515/515.json')
-  .expectJSON({ name: 'deployment', value: 'project not found' })
+  .expectBadge({ label: 'deployment', message: 'project not found' })
 
 t.create('unknown user')
   .get('/this-repo/does-not-exist/1/2.json')
-  .expectJSON({ name: 'deployment', value: 'user or environment not found' })
+  .expectBadge({
+    label: 'deployment',
+    message: 'user or environment not found',
+  })
diff --git a/services/azure-devops/azure-devops-tests.tester.js b/services/azure-devops/azure-devops-tests.tester.js
index fb2bf20fc2b4ac6b9be0671a48a52ab261240355..bcc1bc8f226216de88194e45b05beb8d8ff68f04 100644
--- a/services/azure-devops/azure-devops-tests.tester.js
+++ b/services/azure-devops/azure-devops-tests.tester.js
@@ -116,7 +116,7 @@ const isCompactCustomAzureDevOpsTestTotals = isAzureDevOpsTestTotals(
 
 t.create('unknown build definition')
   .get(`${uriPrefix}/${nonExistentDefinitionId}.json`)
-  .expectJSON({ name: 'tests', value: 'build pipeline not found' })
+  .expectBadge({ label: 'tests', message: 'build pipeline not found' })
 
 t.create('404 latest build error response')
   .get(mockBadgeUri)
@@ -125,9 +125,9 @@ t.create('404 latest build error response')
       .get(mockLatestBuildApiUriPath)
       .reply(404)
   )
-  .expectJSON({
-    name: 'tests',
-    value: 'build pipeline or test result summary not found',
+  .expectBadge({
+    label: 'tests',
+    message: 'build pipeline or test result summary not found',
   })
 
 t.create('no build response')
@@ -140,7 +140,7 @@ t.create('no build response')
         value: [],
       })
   )
-  .expectJSON({ name: 'tests', value: 'build pipeline not found' })
+  .expectBadge({ label: 'tests', message: 'build pipeline not found' })
 
 t.create('no test result summary response')
   .get(mockBadgeUri)
@@ -151,9 +151,9 @@ t.create('no test result summary response')
       .get(mockTestResultSummaryApiUriPath)
       .reply(404)
   )
-  .expectJSON({
-    name: 'tests',
-    value: 'build pipeline or test result summary not found',
+  .expectBadge({
+    label: 'tests',
+    message: 'build pipeline or test result summary not found',
   })
 
 t.create('invalid test result summary response')
@@ -165,7 +165,7 @@ t.create('invalid test result summary response')
       .get(mockTestResultSummaryApiUriPath)
       .reply(200, {})
   )
-  .expectJSON({ name: 'tests', value: 'invalid response data' })
+  .expectBadge({ label: 'tests', message: 'invalid response data' })
 
 t.create('no tests in test result summary response')
   .get(mockBadgeUri)
@@ -176,27 +176,23 @@ t.create('no tests in test result summary response')
       .get(mockTestResultSummaryApiUriPath)
       .reply(200, mockEmptyTestResultSummaryResponse)
   )
-  .expectJSON({ name: 'tests', value: 'no tests' })
+  .expectBadge({ label: 'tests', message: 'no tests' })
 
 t.create('test status')
   .get(mockBadgeUri)
   .intercept(mockTestResultSummarySetup)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: expectedDefaultAzureDevOpsTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: expectedDefaultAzureDevOpsTestTotals,
+  })
 
 t.create('test status on branch')
   .get(mockBranchBadgeUri)
   .intercept(mockBranchTestResultSummarySetup)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: expectedDefaultAzureDevOpsTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: expectedDefaultAzureDevOpsTestTotals,
+  })
 
 t.create('test status with compact message')
   .get(mockBadgeUri, {
@@ -205,12 +201,10 @@ t.create('test status with compact message')
     },
   })
   .intercept(mockTestResultSummarySetup)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: expectedCompactAzureDevOpsTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: expectedCompactAzureDevOpsTestTotals,
+  })
 
 t.create('test status with custom labels')
   .get(mockBadgeUri, {
@@ -221,12 +215,10 @@ t.create('test status with custom labels')
     },
   })
   .intercept(mockTestResultSummarySetup)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: expectedCustomAzureDevOpsTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: expectedCustomAzureDevOpsTestTotals,
+  })
 
 t.create('test status with compact message and custom labels')
   .get(mockBadgeUri, {
@@ -238,24 +230,18 @@ t.create('test status with compact message and custom labels')
     },
   })
   .intercept(mockTestResultSummarySetup)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: expectedCompactCustomAzureDevOpsTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: expectedCompactCustomAzureDevOpsTestTotals,
+  })
 
 t.create('live test status')
   .get(mockBadgeUri)
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isDefaultAzureDevOpsTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isDefaultAzureDevOpsTestTotals })
 
 t.create('live test status on branch')
   .get(mockBranchBadgeUri)
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isDefaultAzureDevOpsTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isDefaultAzureDevOpsTestTotals })
 
 t.create('live test status with compact message')
   .get(mockBadgeUri, {
@@ -263,9 +249,7 @@ t.create('live test status with compact message')
       compact_message: null,
     },
   })
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isCompactAzureDevOpsTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isCompactAzureDevOpsTestTotals })
 
 t.create('live test status with custom labels')
   .get(mockBadgeUri, {
@@ -275,9 +259,7 @@ t.create('live test status with custom labels')
       skipped_label: 'n/a',
     },
   })
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'tests', value: isCustomAzureDevOpsTestTotals })
-  )
+  .expectBadge({ label: 'tests', message: isCustomAzureDevOpsTestTotals })
 
 t.create('live test status with compact message and custom labels')
   .get(mockBadgeUri, {
@@ -288,9 +270,7 @@ t.create('live test status with compact message and custom labels')
       skipped_label: '🤷',
     },
   })
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tests',
-      value: isCompactCustomAzureDevOpsTestTotals,
-    })
-  )
+  .expectBadge({
+    label: 'tests',
+    message: isCompactCustomAzureDevOpsTestTotals,
+  })
diff --git a/services/beerpay/beerpay.tester.js b/services/beerpay/beerpay.tester.js
index 64b83ba222b0c97c0b0a03b5c98114053529e8de..c9cf089de3d5cbcc81de105262119dcfebe806d6 100644
--- a/services/beerpay/beerpay.tester.js
+++ b/services/beerpay/beerpay.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -8,16 +7,14 @@ const amountOfMoney = withRegex(/^\$[0-9]+(\.[0-9]+)?/)
 
 t.create('funding')
   .get('/hashdog/scrapfy-chrome-extension.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'beerpay',
-      value: amountOfMoney,
-    })
-  )
+  .expectBadge({
+    label: 'beerpay',
+    message: amountOfMoney,
+  })
 
 t.create('funding (unknown project)')
   .get('/hashdog/not-a-real-project.json')
-  .expectJSON({
-    name: 'beerpay',
-    value: 'project not found',
+  .expectBadge({
+    label: 'beerpay',
+    message: 'project not found',
   })
diff --git a/services/bintray/bintray.tester.js b/services/bintray/bintray.tester.js
index 38664c165545b077911ff7174bbac94213b424be..720a7cd58d5eab7ad1c47b2f829381e836345d55 100644
--- a/services/bintray/bintray.tester.js
+++ b/services/bintray/bintray.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const {
   isVPlusDottedVersionNClausesWithOptionalSuffix,
 } = require('../test-validators')
@@ -8,21 +7,17 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('version')
   .get('/asciidoctor/maven/asciidoctorj.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bintray',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'bintray',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (not found)')
   .get('/asciidoctor/maven/not-a-real-package.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bintray',
-      value: 'not found',
-    })
-  )
+  .expectBadge({
+    label: 'bintray',
+    message: 'not found',
+  })
 
 t.create('version (mocked)')
   .get('/asciidoctor/maven/asciidoctorj.json?style=_shields_test')
@@ -33,8 +28,8 @@ t.create('version (mocked)')
         name: '1.5.7',
       })
   )
-  .expectJSON({
-    name: 'bintray',
-    value: 'v1.5.7',
+  .expectBadge({
+    label: 'bintray',
+    message: 'v1.5.7',
     color: 'blue',
   })
diff --git a/services/bitbucket/bitbucket.tester.js b/services/bitbucket/bitbucket.tester.js
index 0ce8e7f31e9e1e5c1feebdd6f89518d4d35ad474..2b151968d2402c82eb5a7fea9a7d3965f513978b 100644
--- a/services/bitbucket/bitbucket.tester.js
+++ b/services/bitbucket/bitbucket.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric, isMetricOpenIssues } = require('../test-validators')
 const { isBuildStatus } = require('../build-status')
@@ -21,73 +20,65 @@ const t = (module.exports = new ServiceTester({
 
 t.create('issues-raw (valid)')
   .get('/issues-raw/atlassian/python-bitbucket.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issues',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'issues',
+    message: isMetric,
+  })
 
 t.create('issues-raw (not found)')
   .get('/issues-raw/atlassian/not-a-repo.json')
-  .expectJSON({ name: 'issues', value: 'not found' })
+  .expectBadge({ label: 'issues', message: 'not found' })
 
 t.create('issues-raw (private repo)')
   .get('/issues-raw/chris48s/example-private-repo.json')
-  .expectJSON({ name: 'issues', value: 'private repo' })
+  .expectBadge({ label: 'issues', message: 'private repo' })
 
 t.create('issues (valid)')
   .get('/issues/atlassian/python-bitbucket.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issues',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'issues',
+    message: isMetricOpenIssues,
+  })
 
 t.create('issues (not found)')
   .get('/issues/atlassian/not-a-repo.json')
-  .expectJSON({ name: 'issues', value: 'not found' })
+  .expectBadge({ label: 'issues', message: 'not found' })
 
 t.create('issues (private repo)')
   .get('/issues/chris48s/example-private-repo.json')
-  .expectJSON({ name: 'issues', value: 'private repo' })
+  .expectBadge({ label: 'issues', message: 'private repo' })
 
 // tests for pull requests endpoints
 
 t.create('pr-raw (valid)')
   .get('/pr-raw/atlassian/python-bitbucket.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: isMetric,
+  })
 
 t.create('pr-raw (not found)')
   .get('/pr-raw/atlassian/not-a-repo.json')
-  .expectJSON({ name: 'pull requests', value: 'not found' })
+  .expectBadge({ label: 'pull requests', message: 'not found' })
 
 t.create('pr-raw (private repo)')
   .get('/pr-raw/chris48s/example-private-repo.json')
-  .expectJSON({ name: 'pull requests', value: 'private repo' })
+  .expectBadge({ label: 'pull requests', message: 'private repo' })
 
 t.create('pr (valid)')
   .get('/pr/atlassian/python-bitbucket.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: isMetricOpenIssues,
+  })
 
 t.create('pr (not found)')
   .get('/pr/atlassian/not-a-repo.json')
-  .expectJSON({ name: 'pull requests', value: 'not found' })
+  .expectBadge({ label: 'pull requests', message: 'not found' })
 
 t.create('pr (private repo)')
   .get('/pr/chris48s/example-private-repo.json')
-  .expectJSON({ name: 'pull requests', value: 'private repo' })
+  .expectBadge({ label: 'pull requests', message: 'private repo' })
 
 t.create('pr (server)')
   .get('/pr/project/repo.json?server=https://bitbucket.mydomain.net')
@@ -102,12 +93,10 @@ t.create('pr (server)')
       })
       .reply(200, { size: 42 })
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: isMetricOpenIssues,
+  })
 
 t.create('pr (server, invalid credentials)')
   .get('/pr/project/repo.json?server=https://bitbucket.mydomain.net')
@@ -122,12 +111,10 @@ t.create('pr (server, invalid credentials)')
       })
       .reply(401)
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: 'invalid credentials',
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: 'invalid credentials',
+  })
 
 t.create('pr (server, private repo)')
   .get('/pr/project/repo.json?server=https://bitbucket.mydomain.net')
@@ -142,12 +129,10 @@ t.create('pr (server, private repo)')
       })
       .reply(403)
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: 'private repo',
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: 'private repo',
+  })
 
 t.create('pr (server, not found)')
   .get('/pr/project/repo.json?server=https://bitbucket.mydomain.net')
@@ -162,12 +147,10 @@ t.create('pr (server, not found)')
       })
       .reply(404)
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: 'not found',
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: 'not found',
+  })
 
 t.create('pr (auth)')
   .before(mockBitbucketCreds)
@@ -179,12 +162,10 @@ t.create('pr (auth)')
       .reply(200, { size: 42 })
   )
   .finally(restore)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: isMetricOpenIssues,
+  })
 
 t.create('pr (server, auth)')
   .before(mockBitbucketServerCreds)
@@ -196,12 +177,10 @@ t.create('pr (server, auth)')
       .reply(200, { size: 42 })
   )
   .finally(restore)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: isMetricOpenIssues,
+  })
 // tests for Bitbucket Pipelines
 
 function bitbucketApiResponse(status) {
@@ -223,35 +202,31 @@ function bitbucketApiResponse(status) {
 
 t.create('master build result (valid)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('master build result (not found)')
   .get('/pipelines/atlassian/not-a-repo.json')
-  .expectJSON({ name: 'build', value: 'not found' })
+  .expectBadge({ label: 'build', message: 'not found' })
 
 t.create('branch build result (valid)')
   .get(
     '/pipelines/atlassian/adf-builder-javascript/shields-test-dont-remove.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('branch build result (not found)')
   .get('/pipelines/atlassian/not-a-repo/some-branch.json')
-  .expectJSON({ name: 'build', value: 'not found' })
+  .expectBadge({ label: 'build', message: 'not found' })
 
 t.create('branch build result (never built)')
   .get('/pipelines/atlassian/adf-builder-javascript/some/new/branch.json')
-  .expectJSON({ name: 'build', value: 'never built' })
+  .expectBadge({ label: 'build', message: 'never built' })
 
 t.create('build result (passing)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
@@ -260,7 +235,7 @@ t.create('build result (passing)')
       .get(/^\/2.0\/.*/)
       .reply(200, bitbucketApiResponse('SUCCESSFUL'))
   )
-  .expectJSON({ name: 'build', value: 'passing' })
+  .expectBadge({ label: 'build', message: 'passing' })
 
 t.create('build result (failing)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
@@ -269,7 +244,7 @@ t.create('build result (failing)')
       .get(/^\/2.0\/.*/)
       .reply(200, bitbucketApiResponse('FAILED'))
   )
-  .expectJSON({ name: 'build', value: 'failing' })
+  .expectBadge({ label: 'build', message: 'failing' })
 
 t.create('build result (error)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
@@ -278,7 +253,7 @@ t.create('build result (error)')
       .get(/^\/2.0\/.*/)
       .reply(200, bitbucketApiResponse('ERROR'))
   )
-  .expectJSON({ name: 'build', value: 'error' })
+  .expectBadge({ label: 'build', message: 'error' })
 
 t.create('build result (stopped)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
@@ -287,7 +262,7 @@ t.create('build result (stopped)')
       .get(/^\/2.0\/.*/)
       .reply(200, bitbucketApiResponse('STOPPED'))
   )
-  .expectJSON({ name: 'build', value: 'stopped' })
+  .expectBadge({ label: 'build', message: 'stopped' })
 
 t.create('build result (expired)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
@@ -296,7 +271,7 @@ t.create('build result (expired)')
       .get(/^\/2.0\/.*/)
       .reply(200, bitbucketApiResponse('EXPIRED'))
   )
-  .expectJSON({ name: 'build', value: 'expired' })
+  .expectBadge({ label: 'build', message: 'expired' })
 
 t.create('build result (unexpected status)')
   .get('/pipelines/atlassian/adf-builder-javascript.json')
@@ -305,4 +280,4 @@ t.create('build result (unexpected status)')
       .get(/^\/2.0\/.*/)
       .reply(200, bitbucketApiResponse('NEW_AND_UNEXPECTED'))
   )
-  .expectJSON({ name: 'build', value: 'invalid response data' })
+  .expectBadge({ label: 'build', message: 'invalid response data' })
diff --git a/services/bithound/bithound.tester.js b/services/bithound/bithound.tester.js
index 2d7dd97d328d47356e83d0a2d199b4ff18b9b4d5..f4c70181654b1ce62f9a14a71ee262729d85e34b 100644
--- a/services/bithound/bithound.tester.js
+++ b/services/bithound/bithound.tester.js
@@ -9,21 +9,21 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (code)')
   .get('/code/github/rexxars/sse-channel.json')
-  .expectJSON({
-    name: 'bithound',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'bithound',
+    message: 'no longer available',
   })
 
 t.create('no longer available (dependencies)')
   .get('/dependencies/github/rexxars/sse-channel.json')
-  .expectJSON({
-    name: 'bithound',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'bithound',
+    message: 'no longer available',
   })
 
 t.create('no longer available (devDpendencies)')
   .get('/devDependencies/github/rexxars/sse-channel.json')
-  .expectJSON({
-    name: 'bithound',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'bithound',
+    message: 'no longer available',
   })
diff --git a/services/bountysource/bountysource.tester.js b/services/bountysource/bountysource.tester.js
index 9c4995974ab312cf710ae81c0bea4ca350c09738..634b8adbbc76401c3e78a3df3b2262f240537cfe 100644
--- a/services/bountysource/bountysource.tester.js
+++ b/services/bountysource/bountysource.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const { ServiceTester } = require('../tester')
 
@@ -11,16 +10,14 @@ const t = (module.exports = new ServiceTester({
 
 t.create('bounties (valid)')
   .get('/team/mozilla-core/activity.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bounties',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'bounties',
+    message: isMetric,
+  })
 
 t.create('bounties (invalid team)')
   .get('/team/not-a-real-team/activity.json')
-  .expectJSON({
-    name: 'bounties',
-    value: 'not found',
+  .expectBadge({
+    label: 'bounties',
+    message: 'not found',
   })
diff --git a/services/bower/bower-license.tester.js b/services/bower/bower-license.tester.js
index 09fa02c4002fe71163040fce60cd288dbc83db3a..dc89bdaf962795ad13e104e32a7a7aa97c03ba88 100644
--- a/services/bower/bower-license.tester.js
+++ b/services/bower/bower-license.tester.js
@@ -5,7 +5,7 @@ const t = (module.exports = require('../tester').createServiceTester())
 t.create('licence')
   .timeout(10000)
   .get('/bootstrap.json')
-  .expectJSON({ name: 'license', value: 'MIT' })
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 t.create('license not declared')
   .get('/bootstrap.json')
@@ -14,9 +14,9 @@ t.create('license not declared')
       .get('/api/bower/bootstrap')
       .reply(200, { normalized_licenses: [] })
   )
-  .expectJSON({ name: 'license', value: 'missing' })
+  .expectBadge({ label: 'license', message: 'missing' })
 
 t.create('licence for Invalid Package')
   .timeout(10000)
   .get('/it-is-a-invalid-package-should-error.json')
-  .expectJSON({ name: 'license', value: 'package not found' })
+  .expectBadge({ label: 'license', message: 'package not found' })
diff --git a/services/bower/bower-version.tester.js b/services/bower/bower-version.tester.js
index 447e871f10a223cc285a2929ee4a111c1efca02e..15ed9f06d07069eb2694186d219142099ad409f9 100644
--- a/services/bower/bower-version.tester.js
+++ b/services/bower/bower-version.tester.js
@@ -11,32 +11,28 @@ const isBowerPrereleaseVersion = Joi.string().regex(
 t.create('version')
   .timeout(10000)
   .get('/v/bootstrap.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bower',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'bower',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('pre version') // e.g. bower|v0.2.5-alpha-rc-pre
   .timeout(10000)
   .get('/vpre/bootstrap.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bower',
-      value: isBowerPrereleaseVersion,
-    })
-  )
+  .expectBadge({
+    label: 'bower',
+    message: isBowerPrereleaseVersion,
+  })
 
 t.create('Version for Invalid Package')
   .timeout(10000)
   .get('/v/it-is-a-invalid-package-should-error.json')
-  .expectJSON({ name: 'bower', value: 'package not found' })
+  .expectBadge({ label: 'bower', message: 'package not found' })
 
 t.create('Pre Version for Invalid Package')
   .timeout(10000)
   .get('/vpre/it-is-a-invalid-package-should-error.json')
-  .expectJSON({ name: 'bower', value: 'package not found' })
+  .expectBadge({ label: 'bower', message: 'package not found' })
 
 t.create('Version label should be `no releases` if no stable version')
   .get('/v/bootstrap.json')
@@ -45,7 +41,7 @@ t.create('Version label should be `no releases` if no stable version')
       .get('/api/bower/bootstrap')
       .reply(200, { normalized_licenses: [], latest_stable_release: null })
   )
-  .expectJSON({ name: 'bower', value: 'no releases' })
+  .expectBadge({ label: 'bower', message: 'no releases' })
 
 t.create('Version label should be `no releases` if no pre-release')
   .get('/vpre/bootstrap.json')
@@ -54,4 +50,4 @@ t.create('Version label should be `no releases` if no pre-release')
       .get('/api/bower/bootstrap')
       .reply(200, { normalized_licenses: [], latest_release_number: null })
   )
-  .expectJSON({ name: 'bower', value: 'no releases' })
+  .expectBadge({ label: 'bower', message: 'no releases' })
diff --git a/services/bstats/bstats-players.tester.js b/services/bstats/bstats-players.tester.js
index b2b4a01bb78658243f99baac4a23121848cce3e1..e7d6d1995b782886cb5fb9227f8fdae946b827a9 100644
--- a/services/bstats/bstats-players.tester.js
+++ b/services/bstats/bstats-players.tester.js
@@ -1,14 +1,11 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Players')
   .get('/1.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'players',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'players',
+    message: isMetric,
+  })
diff --git a/services/bstats/bstats-servers.tester.js b/services/bstats/bstats-servers.tester.js
index 72137a306750636cce0199ab31dd57307f0deb48..b4be985b68502acfb238d8e4e24dbd21cd3a0a59 100644
--- a/services/bstats/bstats-servers.tester.js
+++ b/services/bstats/bstats-servers.tester.js
@@ -1,14 +1,11 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Servers')
   .get('/1.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'servers',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'servers',
+    message: isMetric,
+  })
diff --git a/services/bugzilla/bugzilla.tester.js b/services/bugzilla/bugzilla.tester.js
index 9d2d4ebd377edd1f347086e7f3753d5bcd5b01b5..4f13773f442e14d791ea74e20f182e6451b5ebd8 100644
--- a/services/bugzilla/bugzilla.tester.js
+++ b/services/bugzilla/bugzilla.tester.js
@@ -17,13 +17,11 @@ const bzBugStatus = Joi.equal(
 
 t.create('Bugzilla valid bug status')
   .get('/996038.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bug 996038',
-      value: bzBugStatus,
-    })
-  )
+  .expectBadge({
+    label: 'bug 996038',
+    message: bzBugStatus,
+  })
 
 t.create('Bugzilla invalid bug status')
   .get('/83548978974387943879.json')
-  .expectJSON({ name: 'bugzilla', value: 'not found' })
+  .expectBadge({ label: 'bugzilla', message: 'not found' })
diff --git a/services/buildkite/buildkite.tester.js b/services/buildkite/buildkite.tester.js
index de2f5069fb674d3f959fa01037158db3684941b9..1a08e059c4437c1a1866bce412ad7ef0a1b2bc2a 100644
--- a/services/buildkite/buildkite.tester.js
+++ b/services/buildkite/buildkite.tester.js
@@ -12,36 +12,32 @@ const t = (module.exports = new ServiceTester({
 
 t.create('buildkite invalid pipeline')
   .get('/unknown-identifier/unknown-branch.json')
-  .expectJSON({ name: 'build', value: 'not found' })
+  .expectBadge({ label: 'build', message: 'not found' })
 
 t.create('buildkite valid pipeline')
   .get('/3826789cf8890b426057e6fe1c4e683bdf04fa24d498885489/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('buildkite valid pipeline skipping branch')
   .get('/3826789cf8890b426057e6fe1c4e683bdf04fa24d498885489.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('buildkite unknown branch')
   .get(
     '/3826789cf8890b426057e6fe1c4e683bdf04fa24d498885489/unknown-branch.json'
   )
-  .expectJSON({ name: 'build', value: 'unknown' })
+  .expectBadge({ label: 'build', message: 'unknown' })
 
 t.create('buildkite connection error')
   .get('/_.json')
   .networkOff()
-  .expectJSON({ name: 'build', value: 'inaccessible' })
+  .expectBadge({ label: 'build', message: 'inaccessible' })
 
 t.create('buildkite unexpected response')
   .get('/3826789cf8890b426057e6fe1c4e683bdf04fa24d498885489.json')
@@ -52,4 +48,4 @@ t.create('buildkite unexpected response')
       )
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'build', value: 'invalid' })
+  .expectBadge({ label: 'build', message: 'invalid' })
diff --git a/services/bundlephobia/bundlephobia.tester.js b/services/bundlephobia/bundlephobia.tester.js
index f815dd06f12908493b312e7708403c5cc148a7f3..549ed30433840e395f3e456f869cf448995805ff 100644
--- a/services/bundlephobia/bundlephobia.tester.js
+++ b/services/bundlephobia/bundlephobia.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFileSize } = require('../test-validators')
 const { ServiceTester } = require('../tester')
 
@@ -20,57 +19,57 @@ const data = [
   {
     format: formats.A,
     get: '/min/preact.json',
-    expect: { name: 'minified size', value: isFileSize },
+    expect: { label: 'minified size', message: isFileSize },
   },
   {
     format: formats.B,
     get: '/min/preact/8.0.0.json',
-    expect: { name: 'minified size', value: isFileSize },
+    expect: { label: 'minified size', message: isFileSize },
   },
   {
     format: formats.C,
     get: '/min/@cycle/core.json',
-    expect: { name: 'minified size', value: isFileSize },
+    expect: { label: 'minified size', message: isFileSize },
   },
   {
     format: formats.D,
     get: '/min/@cycle/core/7.0.0.json',
-    expect: { name: 'minified size', value: isFileSize },
+    expect: { label: 'minified size', message: isFileSize },
   },
   {
     format: formats.A,
     get: '/minzip/preact.json',
-    expect: { name: 'minzipped size', value: isFileSize },
+    expect: { label: 'minzipped size', message: isFileSize },
   },
   {
     format: formats.B,
     get: '/minzip/preact/8.0.0.json',
-    expect: { name: 'minzipped size', value: isFileSize },
+    expect: { label: 'minzipped size', message: isFileSize },
   },
   {
     format: formats.C,
     get: '/minzip/@cycle/core.json',
-    expect: { name: 'minzipped size', value: isFileSize },
+    expect: { label: 'minzipped size', message: isFileSize },
   },
   {
     format: formats.D,
     get: '/minzip/@cycle/core/7.0.0.json',
-    expect: { name: 'minzipped size', value: isFileSize },
+    expect: { label: 'minzipped size', message: isFileSize },
   },
   {
     format: formats.A,
     get: '/min/some-no-exist.json',
-    expect: { name: 'minified size', value: 'package not found error' },
+    expect: { label: 'minified size', message: 'package not found error' },
   },
   {
     format: formats.C,
     get: '/min/@some-no-exist/some-no-exist.json',
-    expect: { name: 'minified size', value: 'package not found error' },
+    expect: { label: 'minified size', message: 'package not found error' },
   },
 ]
 
 data.forEach(({ format, get, expect }) => {
   t.create(`Testing format '${format}' against '${get}'`)
     .get(get)
-    .expectJSONTypes(Joi.object().keys(expect))
+    .expectBadge(expect)
 })
diff --git a/services/cauditor/cauditor.tester.js b/services/cauditor/cauditor.tester.js
index e971d175f8b02af38594b2f5058421918ae28b2b..aa23655b92df4eff9dab625a47cc56485c1124e1 100644
--- a/services/cauditor/cauditor.tester.js
+++ b/services/cauditor/cauditor.tester.js
@@ -9,7 +9,7 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available')
   .get('/mi/matthiasmullie/scrapbook/master.json')
-  .expectJSON({
-    name: 'cauditor',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'cauditor',
+    message: 'no longer available',
   })
diff --git a/services/cdnjs/cdnjs.tester.js b/services/cdnjs/cdnjs.tester.js
index bfda78a78b2d919e58ebcf811ee427664916b9da..394384d065e8d939ac488bbf1161102ef5f820ce 100644
--- a/services/cdnjs/cdnjs.tester.js
+++ b/services/cdnjs/cdnjs.tester.js
@@ -1,18 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusTripleDottedVersion } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('cdnjs (valid)')
   .get('/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'cdnjs',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'cdnjs',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('cdnjs (not found)')
   .get('/not-a-library.json')
-  .expectJSON({ name: 'cdnjs', value: 'not found' })
+  .expectBadge({ label: 'cdnjs', message: 'not found' })
diff --git a/services/chocolatey/chocolatey.tester.js b/services/chocolatey/chocolatey.tester.js
index 981924470de8e1d1f3ab452139aea018769e6189..7c8ab077e3f6c485f50a7c5d612041b2a09d5cbf 100644
--- a/services/chocolatey/chocolatey.tester.js
+++ b/services/chocolatey/chocolatey.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const {
   isMetric,
   isVPlusDottedVersionNClauses,
@@ -17,43 +16,37 @@ const t = (module.exports = new ServiceTester({
 
 t.create('total downloads (valid)')
   .get('/dt/scriptcs.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (not found)')
   .get('/dt/not-a-real-package.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 // version
 
 t.create('version (valid)')
   .get('/v/scriptcs.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'chocolatey',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'chocolatey',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (not found)')
   .get('/v/not-a-real-package.json')
-  .expectJSON({ name: 'chocolatey', value: 'not found' })
+  .expectBadge({ label: 'chocolatey', message: 'not found' })
 
 // version (pre)
 
 t.create('version (pre) (valid)')
   .get('/vpre/scriptcs.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'chocolatey',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'chocolatey',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (pre) (not found)')
   .get('/vpre/not-a-real-package.json')
-  .expectJSON({ name: 'chocolatey', value: 'not found' })
+  .expectBadge({ label: 'chocolatey', message: 'not found' })
diff --git a/services/chrome-web-store/chrome-web-store.tester.js b/services/chrome-web-store/chrome-web-store.tester.js
index 433441b6f714ef42ba34914b6495ad82d1bee08b..bcca4c6ce67a25cf89e8c9414ce8bc58223a0103 100644
--- a/services/chrome-web-store/chrome-web-store.tester.js
+++ b/services/chrome-web-store/chrome-web-store.tester.js
@@ -15,48 +15,42 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Downloads (now users)')
   .get('/d/alhjnofcnnpeaphgeakdhkebafjcpeae.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'users', value: isMetric }))
+  .expectBadge({ label: 'users', message: isMetric })
 
 t.create('Users')
   .get('/users/alhjnofcnnpeaphgeakdhkebafjcpeae.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'users', value: isMetric }))
+  .expectBadge({ label: 'users', message: isMetric })
 
 t.create('Version')
   .get('/v/alhjnofcnnpeaphgeakdhkebafjcpeae.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'chrome web store',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'chrome web store',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Version - Custom label')
   .get('/v/alhjnofcnnpeaphgeakdhkebafjcpeae.json?label=IndieGala Helper')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'IndieGala Helper',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'IndieGala Helper',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Rating')
   .get('/rating/alhjnofcnnpeaphgeakdhkebafjcpeae.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: Joi.string().regex(/^\d\.?\d+?\/5$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: Joi.string().regex(/^\d\.?\d+?\/5$/),
+  })
 
 t.create('Stars')
   .get('/stars/alhjnofcnnpeaphgeakdhkebafjcpeae.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'rating', value: isStarRating }))
+  .expectBadge({ label: 'rating', message: isStarRating })
 
 t.create('Invalid addon')
   .get('/d/invalid-name-of-addon.json')
-  .expectJSON({ name: 'chrome web store', value: 'invalid' })
+  .expectBadge({ label: 'chrome web store', message: 'invalid' })
 
 t.create('No connection')
   .get('/v/alhjnofcnnpeaphgeakdhkebafjcpeae.json')
   .networkOff()
-  .expectJSON({ name: 'chrome web store', value: 'inaccessible' })
+  .expectBadge({ label: 'chrome web store', message: 'inaccessible' })
diff --git a/services/cii-best-practices/cii-best-practices.tester.js b/services/cii-best-practices/cii-best-practices.tester.js
index bf590eb63303c1ed03b05fe13db882e8ab2df661..f8dce481cfb8f2def24b87b76c49f3626844e390 100644
--- a/services/cii-best-practices/cii-best-practices.tester.js
+++ b/services/cii-best-practices/cii-best-practices.tester.js
@@ -1,39 +1,32 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('live: level known project')
   .get(`/level/1.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'cii',
-      value: withRegex(/in progress|passing|silver|gold/),
-    })
-  )
+  .expectBadge({
+    label: 'cii',
+    message: withRegex(/in progress|passing|silver|gold/),
+  })
 
 t.create('live: percentage known project')
   .get(`/percentage/29.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'cii',
-      value: withRegex(/([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9]|300)%/),
-    })
-  )
+  .expectBadge({
+    label: 'cii',
+    message: withRegex(/([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9]|300)%/),
+  })
 
 t.create('live: summary known project')
   .get(`/summary/33.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'cii',
-      value: withRegex(/(in progress [0-9]|[1-9][0-9]%)|passing|silver|gold/),
-    })
-  )
+  .expectBadge({
+    label: 'cii',
+    message: withRegex(/(in progress [0-9]|[1-9][0-9]%)|passing|silver|gold/),
+  })
 
 t.create('live: unknown project')
   .get(`/level/abc.json`)
-  .expectJSON({ name: 'cii', value: 'project not found' })
+  .expectBadge({ label: 'cii', message: 'project not found' })
 
 t.create('level: gold project')
   .get(`/level/1.json`)
@@ -45,9 +38,9 @@ t.create('level: gold project')
         tiered_percentage: 300,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'gold',
+  .expectBadge({
+    label: 'cii',
+    message: 'gold',
   })
 
 t.create('level: silver project')
@@ -60,9 +53,9 @@ t.create('level: silver project')
         tiered_percentage: 297,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'silver',
+  .expectBadge({
+    label: 'cii',
+    message: 'silver',
   })
 
 t.create('level: passing project')
@@ -75,9 +68,9 @@ t.create('level: passing project')
         tiered_percentage: 107,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'passing',
+  .expectBadge({
+    label: 'cii',
+    message: 'passing',
   })
 
 t.create('level: in progress project')
@@ -90,9 +83,9 @@ t.create('level: in progress project')
         tiered_percentage: 94,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'in progress',
+  .expectBadge({
+    label: 'cii',
+    message: 'in progress',
   })
 
 t.create('percentage: gold project')
@@ -105,9 +98,9 @@ t.create('percentage: gold project')
         tiered_percentage: 300,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: '300%',
+  .expectBadge({
+    label: 'cii',
+    message: '300%',
   })
 
 t.create('percentage: silver project')
@@ -120,9 +113,9 @@ t.create('percentage: silver project')
         tiered_percentage: 297,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: '297%',
+  .expectBadge({
+    label: 'cii',
+    message: '297%',
   })
 
 t.create('percentage: passing project')
@@ -135,9 +128,9 @@ t.create('percentage: passing project')
         tiered_percentage: 107,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: '107%',
+  .expectBadge({
+    label: 'cii',
+    message: '107%',
   })
 
 t.create('percentage: in progress project')
@@ -150,9 +143,9 @@ t.create('percentage: in progress project')
         tiered_percentage: 94,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: '94%',
+  .expectBadge({
+    label: 'cii',
+    message: '94%',
   })
 
 t.create('summary: gold project')
@@ -165,9 +158,9 @@ t.create('summary: gold project')
         tiered_percentage: 300,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'gold',
+  .expectBadge({
+    label: 'cii',
+    message: 'gold',
   })
 
 t.create('summary: silver project')
@@ -180,9 +173,9 @@ t.create('summary: silver project')
         tiered_percentage: 297,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'silver',
+  .expectBadge({
+    label: 'cii',
+    message: 'silver',
   })
 
 t.create('summary: passing project')
@@ -195,9 +188,9 @@ t.create('summary: passing project')
         tiered_percentage: 107,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'passing',
+  .expectBadge({
+    label: 'cii',
+    message: 'passing',
   })
 
 t.create('summary: in progress project')
@@ -210,7 +203,7 @@ t.create('summary: in progress project')
         tiered_percentage: 94,
       })
   )
-  .expectJSON({
-    name: 'cii',
-    value: 'in progress 94%',
+  .expectBadge({
+    label: 'cii',
+    message: 'in progress 94%',
   })
diff --git a/services/circleci/circleci.tester.js b/services/circleci/circleci.tester.js
index 745cfe96329080e923f6b054c92d1b9a893a0ae3..c0e65901786ebb9810ab1f22cb7b66c1b26b5d76 100644
--- a/services/circleci/circleci.tester.js
+++ b/services/circleci/circleci.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const { ServiceTester } = require('../tester')
 
@@ -11,45 +10,37 @@ const t = (module.exports = new ServiceTester({
 
 t.create('circle ci (valid, without branch)')
   .get('/project/github/RedSparr0w/node-csgo-parser.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('circle ci (valid, with branch)')
   .get('/project/github/RedSparr0w/node-csgo-parser/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('build status with "github" as a default VCS')
   .get('/project/RedSparr0w/node-csgo-parser/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('circle ci (valid, with token)')
   .get(
     '/token/b90b5c49e59a4c67ba3a92f7992587ac7a0408c2/project/github/RedSparr0w/node-csgo-parser/master.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('circle ci (not found)')
   .get('/project/github/PyvesB/EmptyRepo.json')
-  .expectJSON({ name: 'build', value: 'project not found' })
+  .expectBadge({ label: 'build', message: 'project not found' })
 
 t.create('circle ci (no response data)')
   .get('/project/github/RedSparr0w/node-csgo-parser.json')
@@ -60,7 +51,7 @@ t.create('circle ci (no response data)')
       )
       .reply(200)
   )
-  .expectJSON({ name: 'build', value: 'unparseable json response' })
+  .expectBadge({ label: 'build', message: 'unparseable json response' })
 
 // we're passing &limit=1 so we expect exactly one array element
 t.create('circle ci (invalid json)')
@@ -72,8 +63,8 @@ t.create('circle ci (invalid json)')
       )
       .reply(200, [{ status: 'success' }, { status: 'fixed' }])
   )
-  .expectJSON({
-    name: 'build',
-    value: 'invalid response data',
+  .expectBadge({
+    label: 'build',
+    message: 'invalid response data',
     color: 'lightgrey',
   })
diff --git a/services/clojars/clojars-downloads.tester.js b/services/clojars/clojars-downloads.tester.js
index cbfccdd2656a1a7d42a78e833f5b9c686f58d433..8331f433bcdf5ec940d174152e22a2bf10e44b81 100644
--- a/services/clojars/clojars-downloads.tester.js
+++ b/services/clojars/clojars-downloads.tester.js
@@ -1,18 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('clojars downloads (valid)')
   .get('/prismic.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('clojars downloads (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
diff --git a/services/clojars/clojars-version.tester.js b/services/clojars/clojars-version.tester.js
index d61bcc17e2c25084f189ac44865ee2ea4726d760..07dc6482a38b4538c8d083ed10f1528fe71df1d4 100644
--- a/services/clojars/clojars-version.tester.js
+++ b/services/clojars/clojars-version.tester.js
@@ -1,17 +1,14 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('clojars (valid)')
   .get('/prismic.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'clojars',
-      value: /^\[prismic "([0-9][.]?)+"\]$/, // note: https://github.com/badges/shields/pull/431
-    })
-  )
+  .expectBadge({
+    label: 'clojars',
+    message: /^\[prismic "([0-9][.]?)+"\]$/, // note: https://github.com/badges/shields/pull/431
+  })
 
 t.create('clojars (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'clojars', value: 'not found' })
+  .expectBadge({ label: 'clojars', message: 'not found' })
diff --git a/services/cocoapods/cocoapods-apps.tester.js b/services/cocoapods/cocoapods-apps.tester.js
index 0a2be6618fd5ad104492de7d7e2418d7c151dd4d..07c8e865211610c014f86b7cc5483a0cdc925aa0 100644
--- a/services/cocoapods/cocoapods-apps.tester.js
+++ b/services/cocoapods/cocoapods-apps.tester.js
@@ -9,12 +9,12 @@ const t = (module.exports = new ServiceTester({
 
 t.create('apps (valid, weekly)')
   .get('/aw/AFNetworking.json')
-  .expectJSON({ name: 'apps', value: 'no longer available' })
+  .expectBadge({ label: 'apps', message: 'no longer available' })
 
 t.create('apps (valid, total)')
   .get('/at/AFNetworking.json')
-  .expectJSON({ name: 'apps', value: 'no longer available' })
+  .expectBadge({ label: 'apps', message: 'no longer available' })
 
 t.create('apps (not found)')
   .get('/at/not-a-package.json')
-  .expectJSON({ name: 'apps', value: 'no longer available' })
+  .expectBadge({ label: 'apps', message: 'no longer available' })
diff --git a/services/cocoapods/cocoapods-docs.tester.js b/services/cocoapods/cocoapods-docs.tester.js
index 610903b041e021717748a9c962e985d44eb4c00a..8c78313163c70dab87d9894427b543322e2baecc 100644
--- a/services/cocoapods/cocoapods-docs.tester.js
+++ b/services/cocoapods/cocoapods-docs.tester.js
@@ -1,17 +1,14 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('doc percent (valid)')
   .get('/AFNetworking.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docs',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'docs',
+    message: isIntegerPercentage,
+  })
 
 t.create('doc percent (null)')
   .get('/AFNetworking.json')
@@ -20,8 +17,8 @@ t.create('doc percent (null)')
       .get('/api/v1/pods/AFNetworking')
       .reply(200, '{"cocoadocs": {"doc_percent": null}}')
   )
-  .expectJSON({ name: 'docs', value: '0%' })
+  .expectBadge({ label: 'docs', message: '0%' })
 
 t.create('doc percent (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'docs', value: 'not found' })
+  .expectBadge({ label: 'docs', message: 'not found' })
diff --git a/services/cocoapods/cocoapods-downloads.tester.js b/services/cocoapods/cocoapods-downloads.tester.js
index 92f008aa500e09e5976c685a61a0ebe0a9c4c11b..8e66b7470ed5b0dcfe27f749ef32edce96c800e9 100644
--- a/services/cocoapods/cocoapods-downloads.tester.js
+++ b/services/cocoapods/cocoapods-downloads.tester.js
@@ -10,16 +10,16 @@ const t = (module.exports = new ServiceTester({
 
 t.create('downloads (valid, monthly)')
   .get('/dm/AFNetworking.json')
-  .expectJSON({ name: 'downloads', value: 'no longer available' })
+  .expectBadge({ label: 'downloads', message: 'no longer available' })
 
 t.create('downloads (valid, weekly)')
   .get('/dw/AFNetworking.json')
-  .expectJSON({ name: 'downloads', value: 'no longer available' })
+  .expectBadge({ label: 'downloads', message: 'no longer available' })
 
 t.create('downloads (valid, total)')
   .get('/dt/AFNetworking.json')
-  .expectJSON({ name: 'downloads', value: 'no longer available' })
+  .expectBadge({ label: 'downloads', message: 'no longer available' })
 
 t.create('downloads (not found)')
   .get('/dt/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'no longer available' })
+  .expectBadge({ label: 'downloads', message: 'no longer available' })
diff --git a/services/cocoapods/cocoapods-license.tester.js b/services/cocoapods/cocoapods-license.tester.js
index 4bf6c7458d8313193a0d75991972a7192680f479..9a7b71abdfc75d8b6af3a286bb979a1637586a4c 100644
--- a/services/cocoapods/cocoapods-license.tester.js
+++ b/services/cocoapods/cocoapods-license.tester.js
@@ -4,8 +4,8 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('license (valid)')
   .get('/AFNetworking.json')
-  .expectJSON({ name: 'license', value: 'MIT' })
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 t.create('license (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'license', value: 'not found' })
+  .expectBadge({ label: 'license', message: 'not found' })
diff --git a/services/cocoapods/cocoapods-platform.tester.js b/services/cocoapods/cocoapods-platform.tester.js
index 78e2b5ca156f2a61b9610a73fc172386acf01c62..74f8c3f40d08aaf99052ab947e51bbf23495c242 100644
--- a/services/cocoapods/cocoapods-platform.tester.js
+++ b/services/cocoapods/cocoapods-platform.tester.js
@@ -9,16 +9,14 @@ const isPlatform = Joi.string().regex(
 
 t.create('platform (valid)')
   .get('/AFNetworking.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'platform',
-      value: isPlatform,
-    })
-  )
+  .expectBadge({
+    label: 'platform',
+    message: isPlatform,
+  })
 
 t.create('platform (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'platform', value: 'not found' })
+  .expectBadge({ label: 'platform', message: 'not found' })
 
 t.create('platform (missing platforms key)')
   .get('/AFNetworking.json')
@@ -27,4 +25,4 @@ t.create('platform (missing platforms key)')
       .get('/api/v1/pods/AFNetworking/specs/latest')
       .reply(200, { version: 'v1.0', license: 'MIT' })
   )
-  .expectJSON({ name: 'platform', value: 'ios | osx' })
+  .expectBadge({ label: 'platform', message: 'ios | osx' })
diff --git a/services/cocoapods/cocoapods-version.tester.js b/services/cocoapods/cocoapods-version.tester.js
index 624111661c6ee86f9654416744054e98989f82bd..628037601901a10e3df784340f65995159e28458 100644
--- a/services/cocoapods/cocoapods-version.tester.js
+++ b/services/cocoapods/cocoapods-version.tester.js
@@ -1,18 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('version (valid)')
   .get('/AFNetworking.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pod',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'pod',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('version (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'pod', value: 'not found' })
+  .expectBadge({ label: 'pod', message: 'not found' })
diff --git a/services/codacy/codacy-coverage.tester.js b/services/codacy/codacy-coverage.tester.js
index 9007eb3650daf2dbf00dea2a98de49fbe0249580..c159f424754f5871add37c5acdcfa1ba123dcd08 100644
--- a/services/codacy/codacy-coverage.tester.js
+++ b/services/codacy/codacy-coverage.tester.js
@@ -1,37 +1,32 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Coverage')
   .get('/59d607d0e311408885e418004068ea58.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('Coverage on branch')
   .get('/59d607d0e311408885e418004068ea58/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('Coverage not enabled')
   .get('/e27821fb6289410b8f58338c7e0bc686.json')
-  .expectJSON({
-    name: 'coverage',
-    value: 'not enabled for this project',
+  .expectBadge({
+    label: 'coverage',
+    message: 'not enabled for this project',
   })
 
 t.create('Coverage (project not found)')
   .get('/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.json')
-  .expectJSON({
-    name: 'coverage',
-    value: 'project not found',
+  .expectBadge({
+    label: 'coverage',
+    message: 'project not found',
   })
diff --git a/services/codacy/codacy-grade.tester.js b/services/codacy/codacy-grade.tester.js
index 8a0b4aca6bcc825b448dd140891f7fbcf44e6e00..13db31f1b209c84f677361200bbef5fd19d90a93 100644
--- a/services/codacy/codacy-grade.tester.js
+++ b/services/codacy/codacy-grade.tester.js
@@ -1,32 +1,27 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { codacyGrade } = require('./codacy-helpers')
 
 t.create('Code quality')
   .get('/grade/e27821fb6289410b8f58338c7e0bc686.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'code quality',
-      value: codacyGrade,
-    })
-  )
+  .expectBadge({
+    label: 'code quality',
+    message: codacyGrade,
+  })
 
 t.create('Code quality on branch')
   .get('/grade/e27821fb6289410b8f58338c7e0bc686/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'code quality',
-      value: codacyGrade,
-    })
-  )
+  .expectBadge({
+    label: 'code quality',
+    message: codacyGrade,
+  })
 
 t.create('Code quality (package not found)')
   .get('/grade/00000000000000000000000000000000/master.json')
-  .expectJSON({
-    name: 'code quality',
-    value: 'project or branch not found',
+  .expectBadge({
+    label: 'code quality',
+    message: 'project or branch not found',
   })
 
 // This is a known bug. The badge endpoint for a nonexistent branch returns
@@ -37,7 +32,7 @@ t.create('Code quality (package not found)')
 // https://api.codacy.com/project/badge/grade/e27821fb6289410b8f58338c7e0bc686?branch=foo
 // t.create('Code quality on branch (branch not found)')
 //   .get('/grade/e27821fb6289410b8f58338c7e0bc686/not-a-branch.json')
-//   .expectJSON({
-//       name: 'code quality',
-//       value: 'project or branch not found',
+//   .expectBadge({
+//       label: 'code quality',
+//       message: 'project or branch not found',
 //   })
diff --git a/services/codeclimate/codeclimate.tester.js b/services/codeclimate/codeclimate.tester.js
index 38e7da77c0455eac9c7206b0e9072bda9e13767b..d79fe8a61312b66b5288e561dd83ea8f95865570 100644
--- a/services/codeclimate/codeclimate.tester.js
+++ b/services/codeclimate/codeclimate.tester.js
@@ -12,105 +12,89 @@ const t = (module.exports = new ServiceTester({
 // Tests based on Code Climate's test reports endpoint.
 t.create('test coverage percentage')
   .get('/c/jekyll/jekyll.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('test coverage percentage alternative coverage URL')
   .get('/coverage/jekyll/jekyll.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('test coverage percentage alternative top-level URL')
   .get('/jekyll/jekyll.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('test coverage letter')
   .get('/c-letter/jekyll/jekyll.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
+  })
 
 t.create('test coverage percentage for non-existent repo')
   .get('/c/unknown/unknown.json')
-  .expectJSON({
-    name: 'coverage',
-    value: 'not found',
+  .expectBadge({
+    label: 'coverage',
+    message: 'not found',
   })
 
 t.create('test coverage percentage for repo without test reports')
   .get('/c/angular/angular.js.json')
-  .expectJSON({
-    name: 'coverage',
-    value: 'unknown',
+  .expectBadge({
+    label: 'coverage',
+    message: 'unknown',
   })
 
 // Tests based on Code Climate's snapshots endpoint.
 t.create('issues count')
   .get('/issues/angular/angular.js.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issues',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'issues',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('technical debt percentage')
   .get('/tech-debt/angular/angular.js.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'technical debt',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'technical debt',
+    message: isIntegerPercentage,
+  })
 
 t.create('maintainability percentage')
   .get('/maintainability-percentage/angular/angular.js.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'maintainability',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'maintainability',
+    message: isIntegerPercentage,
+  })
 
 t.create('maintainability letter')
   .get('/maintainability/angular/angular.js.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'maintainability',
-      value: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
-    })
-  )
+  .expectBadge({
+    label: 'maintainability',
+    message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
+  })
 
 t.create('maintainability letter for non-existent repo')
   .get('/maintainability/unknown/unknown.json')
-  .expectJSON({
-    name: 'maintainability',
-    value: 'not found',
+  .expectBadge({
+    label: 'maintainability',
+    message: 'not found',
   })
 
 t.create('maintainability letter for repo without snapshots')
   .get('/maintainability/kabisaict/flow.json')
-  .expectJSON({
-    name: 'maintainability',
-    value: 'unknown',
+  .expectBadge({
+    label: 'maintainability',
+    message: 'unknown',
   })
 
 t.create('malformed response for outer user repos query')
@@ -122,9 +106,9 @@ t.create('malformed response for outer user repos query')
         data: [{}], // No relationships in the list of data elements.
       })
   )
-  .expectJSON({
-    name: 'maintainability',
-    value: 'invalid',
+  .expectBadge({
+    label: 'maintainability',
+    message: 'invalid',
   })
 
 t.create('malformed response for inner specific repo query')
@@ -135,7 +119,7 @@ t.create('malformed response for inner specific repo query')
       .reply(200, {})
   ) // No data.
   .networkOn() // Combined with allowUnmocked: true, this allows the outer user repos query to go through.
-  .expectJSON({
-    name: 'maintainability',
-    value: 'invalid',
+  .expectBadge({
+    label: 'maintainability',
+    message: 'invalid',
   })
diff --git a/services/codecov/codecov.tester.js b/services/codecov/codecov.tester.js
index 753d7340f7e0a246c7ed9934ba48e91e27da0e1b..088b167041aa4d52ad8743e8974969cc74071c9f 100644
--- a/services/codecov/codecov.tester.js
+++ b/services/codecov/codecov.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isIntegerPercentage } = require('../test-validators')
 
@@ -11,18 +10,14 @@ const t = (module.exports = new ServiceTester({
 
 t.create('gets coverage status')
   .get('/c/github/codecov/example-python.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('gets coverate status for branch')
   .get('/c/github/codecov/example-python/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
diff --git a/services/codeship/codeship.tester.js b/services/codeship/codeship.tester.js
index cd61b6457aed8b8dc9c119ef6786fddd9f54d15a..d168f9c4a7db62ac1ed71a539a223348c2fa979b 100644
--- a/services/codeship/codeship.tester.js
+++ b/services/codeship/codeship.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const { ServiceTester } = require('../tester')
 
@@ -11,34 +10,30 @@ const t = (module.exports = new ServiceTester({
 
 t.create('codeship (valid, no branch)')
   .get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('codeship (valid, with branch)')
   .get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('codeship (repo not found)')
   .get('/not-a-repo.json')
-  .expectJSON({ name: 'build', value: 'not found' })
+  .expectBadge({ label: 'build', message: 'not found' })
 
 t.create('codeship (branch not found)')
   .get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1/not-a-branch.json')
-  .expectJSON({ name: 'build', value: 'branch not found' })
+  .expectBadge({ label: 'build', message: 'branch not found' })
 
 t.create('codeship (connection error)')
   .get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1.json')
   .networkOff()
-  .expectJSON({ name: 'build', value: 'inaccessible' })
+  .expectBadge({ label: 'build', message: 'inaccessible' })
 
 t.create('codeship (unexpected response header)')
   .get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1.json')
@@ -49,7 +44,7 @@ t.create('codeship (unexpected response header)')
         'content-disposition': 'foo',
       })
   )
-  .expectJSON({ name: 'build', value: 'unknown' })
+  .expectBadge({ label: 'build', message: 'unknown' })
 
 t.create('codeship (unexpected response body)')
   .get('/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1.json')
@@ -58,4 +53,4 @@ t.create('codeship (unexpected response body)')
       .get('/projects/d6c1ddd0-16a3-0132-5f85-2e35c05e22b1/status')
       .reply(200, '')
   )
-  .expectJSON({ name: 'build', value: 'invalid' })
+  .expectBadge({ label: 'build', message: 'invalid' })
diff --git a/services/codetally/codetally.tester.js b/services/codetally/codetally.tester.js
index 1128f3ce24af345648184ec7041e609623d6008c..3fc5e5b0ab1739e466d752e199c098ea9a501f99 100644
--- a/services/codetally/codetally.tester.js
+++ b/services/codetally/codetally.tester.js
@@ -12,12 +12,10 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Codetally')
   .get('/triggerman722/colorstrap.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'codetally',
-      value: Joi.string().regex(/\b\d+(?:.\d+)?/),
-    })
-  )
+  .expectBadge({
+    label: 'codetally',
+    message: Joi.string().regex(/\b\d+(?:.\d+)?/),
+  })
 
 t.create('Empty')
   .get('/triggerman722/colorstrap.json')
@@ -31,9 +29,9 @@ t.create('Empty')
         currency_abbreviation: 'CAD',
       })
   )
-  .expectJSON({ name: 'codetally', value: '$0.00' })
+  .expectBadge({ label: 'codetally', message: '$0.00' })
 
 t.create('Non existent')
   .get('/not/real.json')
   .timeout(10000)
-  .expectJSON({ name: 'codetally', value: 'repo not found' })
+  .expectBadge({ label: 'codetally', message: 'repo not found' })
diff --git a/services/conda/conda.tester.js b/services/conda/conda.tester.js
index db304d6dcf1738a6030b2ecec83b9b966772f269..8d222f21a3aa369a63d98592c4dce08a0e3de15a 100644
--- a/services/conda/conda.tester.js
+++ b/services/conda/conda.tester.js
@@ -10,57 +10,47 @@ const t = (module.exports = new ServiceTester({ id: 'conda', title: 'Conda' }))
 
 t.create('version')
   .get('/v/conda-forge/zlib.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'conda|conda-forge',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'conda|conda-forge',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('version (skip prefix)')
   .get('/vn/conda-forge/zlib.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'conda-forge',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'conda-forge',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('platform')
   .get('/p/conda-forge/zlib.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'conda|platform',
-      value: isCondaPlatform,
-    })
-  )
+  .expectBadge({
+    label: 'conda|platform',
+    message: isCondaPlatform,
+  })
 
 t.create('platform (skip prefix)')
   .get('/pn/conda-forge/zlib.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'platform',
-      value: isCondaPlatform,
-    })
-  )
+  .expectBadge({
+    label: 'platform',
+    message: isCondaPlatform,
+  })
 
 t.create('downloads')
   .get('/d/conda-forge/zlib.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'conda|downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'conda|downloads',
+    message: isMetric,
+  })
 
 t.create('downloads (skip prefix)')
   .get('/dn/conda-forge/zlib.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('unknown package')
   .get('/d/conda-forge/some-bogus-package-that-never-exists.json')
-  .expectJSON({ name: 'conda', value: 'not found' })
+  .expectBadge({ label: 'conda', message: 'not found' })
 
 t.create('unknown channel')
   .get('/d/some-bogus-channel-that-never-exists/zlib.json')
-  .expectJSON({ name: 'conda', value: 'not found' })
+  .expectBadge({ label: 'conda', message: 'not found' })
diff --git a/services/continuousphp/continuousphp.tester.js b/services/continuousphp/continuousphp.tester.js
index 60b211f6e02721d9db7d61e65a6c471283bca071..3935e360b660c89bacc7f5d18d32c393b14aff51 100644
--- a/services/continuousphp/continuousphp.tester.js
+++ b/services/continuousphp/continuousphp.tester.js
@@ -6,27 +6,23 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('build status on default branch')
   .get('/git-hub/doctrine/dbal.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build status on named branch')
   .get('/git-hub/doctrine/dbal/develop.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('unknown repo')
   .get('/git-hub/this-repo/does-not-exist.json')
-  .expectJSON({ name: 'build', value: 'invalid' })
+  .expectBadge({ label: 'build', message: 'invalid' })
 
 t.create('connection error')
   .get('/git-hub/doctrine/dbal.json')
   .networkOff()
-  .expectJSON({ name: 'build', value: 'invalid' })
+  .expectBadge({ label: 'build', message: 'invalid' })
diff --git a/services/cookbook/cookbook.tester.js b/services/cookbook/cookbook.tester.js
index 580d8fc6fc66a796c9694507f27eb4198882974b..09d2c54ddd5caaaba245947bebc519ec1451c445 100644
--- a/services/cookbook/cookbook.tester.js
+++ b/services/cookbook/cookbook.tester.js
@@ -1,17 +1,14 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('version')
   .get('/chef-sugar.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'cookbook',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'cookbook',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('version (mocked)')
   .get('/chef-sugar.json?style=_shields_test')
@@ -22,12 +19,12 @@ t.create('version (mocked)')
         version: '4.1.0',
       })
   )
-  .expectJSON({
-    name: 'cookbook',
-    value: 'v4.1.0',
+  .expectBadge({
+    label: 'cookbook',
+    message: 'v4.1.0',
     color: 'blue',
   })
 
 t.create('version (not found)')
   .get('/not-a-cookbook.json')
-  .expectJSON({ name: 'cookbook', value: 'not found' })
+  .expectBadge({ label: 'cookbook', message: 'not found' })
diff --git a/services/coveralls/coveralls.tester.js b/services/coveralls/coveralls.tester.js
index ab9f826476aeff13697da07b2fe66f87b02c04f5..e9ad4b707582ff114e14887d230440483e186a46 100644
--- a/services/coveralls/coveralls.tester.js
+++ b/services/coveralls/coveralls.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const { ServiceTester } = require('../tester')
 
@@ -16,7 +15,7 @@ t.create('error status code - location header is missing')
       .head('/repos/github/not/existed/badge.svg')
       .reply(404)
   )
-  .expectJSON({ name: 'coverage', value: 'invalid' })
+  .expectBadge({ label: 'coverage', message: 'invalid' })
 
 t.create('malformed location')
   .get('/github/user/repository.json')
@@ -32,7 +31,7 @@ t.create('malformed location')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: 'malformed' })
+  .expectBadge({ label: 'coverage', message: 'malformed' })
 
 t.create('NaN percentage in location')
   .get('/github/user/repository.json')
@@ -48,12 +47,12 @@ t.create('NaN percentage in location')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: 'unknown' })
+  .expectBadge({ label: 'coverage', message: 'unknown' })
 
 t.create('connection error')
   .get('/github/user/repository.json')
   .networkOff()
-  .expectJSON({ name: 'coverage', value: 'invalid' })
+  .expectBadge({ label: 'coverage', message: 'invalid' })
 
 t.create('show coverage')
   .get('/github/user/repository.json')
@@ -69,7 +68,7 @@ t.create('show coverage')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: '50%' })
+  .expectBadge({ label: 'coverage', message: '50%' })
 
 t.create('show coverage for legacy github link')
   .get('/user/repository.json')
@@ -85,7 +84,7 @@ t.create('show coverage for legacy github link')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: '50%' })
+  .expectBadge({ label: 'coverage', message: '50%' })
 
 t.create('show coverage for branch')
   .get('/github/user/repository/branch.json')
@@ -101,7 +100,7 @@ t.create('show coverage for branch')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: '50%' })
+  .expectBadge({ label: 'coverage', message: '50%' })
 
 t.create('show coverage for bitbucket')
   .get('/bitbucket/user/repository.json')
@@ -117,7 +116,7 @@ t.create('show coverage for bitbucket')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: '50%' })
+  .expectBadge({ label: 'coverage', message: '50%' })
 
 t.create('show coverage for bitbucket with branch')
   .get('/bitbucket/user/repository/branch.json')
@@ -133,22 +132,16 @@ t.create('show coverage for bitbucket with branch')
         }
       )
   )
-  .expectJSON({ name: 'coverage', value: '50%' })
+  .expectBadge({ label: 'coverage', message: '50%' })
 
 t.create('github coverage')
   .get('/github/jekyll/jekyll.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'coverage', value: isIntegerPercentage })
-  )
+  .expectBadge({ label: 'coverage', message: isIntegerPercentage })
 
 t.create('github coverage for legacy link')
   .get('/jekyll/jekyll.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'coverage', value: isIntegerPercentage })
-  )
+  .expectBadge({ label: 'coverage', message: isIntegerPercentage })
 
 t.create('bitbucket coverage')
   .get('/bitbucket/pyKLIP/pyklip.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'coverage', value: isIntegerPercentage })
-  )
+  .expectBadge({ label: 'coverage', message: isIntegerPercentage })
diff --git a/services/coverity/coverity-on-demand.tester.js b/services/coverity/coverity-on-demand.tester.js
index 23e957ba711768e7c3ea2d91ebdaf2b1726fe070..09199d915029aec7af789f6b137c4eb2c98579b3 100644
--- a/services/coverity/coverity-on-demand.tester.js
+++ b/services/coverity/coverity-on-demand.tester.js
@@ -10,14 +10,14 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (streams)')
   .get('/streams/44b25sjc9l3ntc2ngfi29tngro.json')
-  .expectJSON({
-    name: 'coverity',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'coverity',
+    message: 'no longer available',
   })
 
 t.create('no longer available (jobs)')
   .get('/jobs/p4tmm8031t4i971r0im4s7lckk.json')
-  .expectJSON({
-    name: 'coverity',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'coverity',
+    message: 'no longer available',
   })
diff --git a/services/coverity/coverity-scan.tester.js b/services/coverity/coverity-scan.tester.js
index 68baeeb26499bd3a6f1d7b306f90ed03f2d5a43e..6d010ad00ce7d37da34a08aa2c95186c9d82f67a 100644
--- a/services/coverity/coverity-scan.tester.js
+++ b/services/coverity/coverity-scan.tester.js
@@ -5,17 +5,15 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('live: known project id')
   .get('/3997.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverity',
-      value: Joi.string().regex(/passing|passed .* new defects|pending|failed/),
-    })
-  )
+  .expectBadge({
+    label: 'coverity',
+    message: Joi.string().regex(/passing|passed .* new defects|pending|failed/),
+  })
 
 t.create('live: unknown project id')
   .get('/abc.json')
   // Coverity actually returns an HTTP 200 status with an HTML page when the project is not found.
-  .expectJSON({ name: 'coverity', value: 'unparseable json response' })
+  .expectBadge({ label: 'coverity', message: 'unparseable json response' })
 
 t.create('404 response')
   .get('/1.json')
@@ -24,7 +22,7 @@ t.create('404 response')
       .get('/badge.json')
       .reply(404)
   )
-  .expectJSON({ name: 'coverity', value: 'project not found' })
+  .expectBadge({ label: 'coverity', message: 'project not found' })
 
 t.create('passed')
   .get('/2.json?style=_shields_test')
@@ -35,9 +33,9 @@ t.create('passed')
         message: 'passed',
       })
   )
-  .expectJSON({
-    name: 'coverity',
-    value: 'passing',
+  .expectBadge({
+    label: 'coverity',
+    message: 'passing',
     color: 'brightgreen',
   })
 
@@ -50,9 +48,9 @@ t.create('passed with defects')
         message: 'passed 51 new defects',
       })
   )
-  .expectJSON({
-    name: 'coverity',
-    value: 'passed 51 new defects',
+  .expectBadge({
+    label: 'coverity',
+    message: 'passed 51 new defects',
     color: 'yellow',
   })
 
@@ -65,9 +63,9 @@ t.create('pending')
         message: 'pending',
       })
   )
-  .expectJSON({
-    name: 'coverity',
-    value: 'pending',
+  .expectBadge({
+    label: 'coverity',
+    message: 'pending',
     color: 'orange',
   })
 
@@ -80,8 +78,8 @@ t.create('failed')
         message: 'failed',
       })
   )
-  .expectJSON({
-    name: 'coverity',
-    value: 'failed',
+  .expectBadge({
+    label: 'coverity',
+    message: 'failed',
     color: 'red',
   })
diff --git a/services/cpan/cpan-license.tester.js b/services/cpan/cpan-license.tester.js
index 3dcd6b21cd973e7d6ff5c971e829080a9883ca7b..c841b2b2fbb0f10b9339352556e886aaab3f183a 100644
--- a/services/cpan/cpan-license.tester.js
+++ b/services/cpan/cpan-license.tester.js
@@ -4,14 +4,14 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('license (valid)')
   .get('/Config-Augeas.json')
-  .expectJSON({
-    name: 'license',
-    value: 'lgpl_2_1',
+  .expectBadge({
+    label: 'license',
+    message: 'lgpl_2_1',
   })
 
 t.create('license (not found)')
   .get('/not-a-package.json')
-  .expectJSON({
-    name: 'cpan',
-    value: 'not found',
+  .expectBadge({
+    label: 'cpan',
+    message: 'not found',
   })
diff --git a/services/cpan/cpan-version.tester.js b/services/cpan/cpan-version.tester.js
index 4659f3271a65e61ae1a6bddb7b2399b8c894738d..1a1e26fa8a5ef4a1b934e8c8e823403cf3960b2c 100644
--- a/services/cpan/cpan-version.tester.js
+++ b/services/cpan/cpan-version.tester.js
@@ -5,14 +5,14 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('version (valid)')
   .get('/Config-Augeas.json')
-  .expectJSONTypes({
-    name: 'cpan',
-    value: isVPlusDottedVersionAtLeastOne,
+  .expectBadge({
+    label: 'cpan',
+    message: isVPlusDottedVersionAtLeastOne,
   })
 
 t.create('version (not found)')
   .get('/not-a-package.json')
-  .expectJSON({
-    name: 'cpan',
-    value: 'not found',
+  .expectBadge({
+    label: 'cpan',
+    message: 'not found',
   })
diff --git a/services/cran/cran.tester.js b/services/cran/cran.tester.js
index decb96612813a397f31f57498c72c7ac10179ab3..92f65d1a7b7b80a77f34be992a82ed4ce00b1825 100644
--- a/services/cran/cran.tester.js
+++ b/services/cran/cran.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isVPlusTripleDottedVersion } = require('../test-validators')
 
@@ -11,21 +10,19 @@ const t = (module.exports = new ServiceTester({
 
 t.create('version (valid)')
   .get('/v/devtools.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'cran',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'cran',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('version (not found)')
   .get('/v/some-bogus-package.json')
-  .expectJSON({ name: 'cran', value: 'not found' })
+  .expectBadge({ label: 'cran', message: 'not found' })
 
 t.create('license (valid)')
   .get('/l/devtools.json')
-  .expectJSON({ name: 'license', value: 'GPL (>= 2)' })
+  .expectBadge({ label: 'license', message: 'GPL (>= 2)' })
 
 t.create('license (not found)')
   .get('/l/some-bogus-package.json')
-  .expectJSON({ name: 'cran', value: 'not found' })
+  .expectBadge({ label: 'cran', message: 'not found' })
diff --git a/services/crates/crates-downloads.tester.js b/services/crates/crates-downloads.tester.js
index 8cb1ddd50bd20a0f73f489a2943aa2b4c3f4bb85..1f81301dec5f713c38fc86625d83ab7d2a1965fd 100644
--- a/services/crates/crates-downloads.tester.js
+++ b/services/crates/crates-downloads.tester.js
@@ -11,33 +11,33 @@ const t = (module.exports = new ServiceTester({
 
 t.create('total downloads')
   .get('/d/libc.json')
-  .expectJSONTypes({ name: 'downloads', value: isMetric })
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('total downloads (with version)')
   .get('/d/libc/0.2.31.json')
-  .expectJSONTypes({
-    name: 'downloads@0.2.31',
-    value: isMetric,
+  .expectBadge({
+    label: 'downloads@0.2.31',
+    message: isMetric,
   })
 
 t.create('downloads for version')
   .get('/dv/libc.json')
-  .expectJSONTypes({
-    name: 'downloads@latest',
-    value: isMetric,
+  .expectBadge({
+    label: 'downloads@latest',
+    message: isMetric,
   })
 
 t.create('downloads for version (with version)')
   .get('/dv/libc/0.2.31.json')
-  .expectJSONTypes({
-    name: 'downloads@0.2.31',
-    value: isMetric,
+  .expectBadge({
+    label: 'downloads@0.2.31',
+    message: isMetric,
   })
 
 t.create('downloads (invalid version)')
   .get('/d/libc/7.json')
-  .expectJSON({ name: 'crates.io', value: 'invalid semver: 7' })
+  .expectBadge({ label: 'crates.io', message: 'invalid semver: 7' })
 
 t.create('downloads (not found)')
   .get('/d/not-a-real-package.json')
-  .expectJSON({ name: 'crates.io', value: 'not found' })
+  .expectBadge({ label: 'crates.io', message: 'not found' })
diff --git a/services/crates/crates-license.tester.js b/services/crates/crates-license.tester.js
index e18a0929e47f41e6f6a98e9448dc50881ef93f62..79d711034ea09fb578a74dfba637778d5ab2dc70 100644
--- a/services/crates/crates-license.tester.js
+++ b/services/crates/crates-license.tester.js
@@ -10,12 +10,12 @@ const t = (module.exports = new ServiceTester({
 
 t.create('license')
   .get('/libc.json')
-  .expectJSON({ name: 'license', value: 'MIT OR Apache-2.0' })
+  .expectBadge({ label: 'license', message: 'MIT OR Apache-2.0' })
 
 t.create('license (with version)')
   .get('/libc/0.2.44.json')
-  .expectJSON({ name: 'license', value: 'MIT OR Apache-2.0' })
+  .expectBadge({ label: 'license', message: 'MIT OR Apache-2.0' })
 
 t.create('license (not found)')
   .get('/not-a-real-package.json')
-  .expectJSON({ name: 'crates.io', value: 'not found' })
+  .expectBadge({ label: 'crates.io', message: 'not found' })
diff --git a/services/crates/crates-version.tester.js b/services/crates/crates-version.tester.js
index c0813eaf290e356eb01f3c935144962b82d63219..c4c8b9b4b4b82a7085af213da162f81163cb8231 100644
--- a/services/crates/crates-version.tester.js
+++ b/services/crates/crates-version.tester.js
@@ -11,8 +11,8 @@ const t = (module.exports = new ServiceTester({
 
 t.create('version')
   .get('/libc.json')
-  .expectJSONTypes({ name: 'crates.io', value: isSemver })
+  .expectBadge({ label: 'crates.io', message: isSemver })
 
 t.create('version (not found)')
   .get('/not-a-real-package.json')
-  .expectJSON({ name: 'crates.io', value: 'not found' })
+  .expectBadge({ label: 'crates.io', message: 'not found' })
diff --git a/services/ctan/ctan.tester.js b/services/ctan/ctan.tester.js
index b02be86e3015e7b546e69e6cef0e2700b2e3063e..0e538ecbfd38b736d258f2b4324b908b754fbd41 100644
--- a/services/ctan/ctan.tester.js
+++ b/services/ctan/ctan.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 
@@ -11,9 +10,9 @@ const t = (module.exports = new ServiceTester({
 
 t.create('license')
   .get('/l/novel.json')
-  .expectJSON({
-    name: 'license',
-    value: 'lppl1.3c, ofl',
+  .expectBadge({
+    label: 'license',
+    message: 'lppl1.3c, ofl',
   })
 
 t.create('license missing')
@@ -27,9 +26,9 @@ t.create('license missing')
         },
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'missing',
+  .expectBadge({
+    label: 'license',
+    message: 'missing',
   })
 
 t.create('single license')
@@ -44,19 +43,17 @@ t.create('single license')
         },
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'knuth',
+  .expectBadge({
+    label: 'license',
+    message: 'knuth',
   })
 
 t.create('version')
   .get('/v/novel.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'ctan',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'ctan',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('version (mocked)')
   .get('/v/novel.json?style=_shields_test')
@@ -69,8 +66,8 @@ t.create('version (mocked)')
         },
       })
   )
-  .expectJSON({
-    name: 'ctan',
-    value: 'v1.11',
+  .expectBadge({
+    label: 'ctan',
+    message: 'v1.11',
     color: 'blue',
   })
diff --git a/services/date/date.tester.js b/services/date/date.tester.js
index c9d9be07adb2533554f3936d652de5f4ee938c05..3e0135bd01d53a250e31ae37a7410ebfa899bd5b 100644
--- a/services/date/date.tester.js
+++ b/services/date/date.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isRelativeFormattedDate } = require('../test-validators')
 
@@ -11,10 +10,8 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Relative date')
   .get('/1540814400.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'date', value: isRelativeFormattedDate })
-  )
+  .expectBadge({ label: 'date', message: isRelativeFormattedDate })
 
 t.create('Relative date - Invalid')
   .get('/9999999999999.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'date', value: 'invalid date' }))
+  .expectBadge({ label: 'date', message: 'invalid date' })
diff --git a/services/david/david.tester.js b/services/david/david.tester.js
index 4cdcc374ce544e977a4db1764ecb5041401fd083..49e1d66f0f6bffecc9fd4095029a8573ae6831f1 100644
--- a/services/david/david.tester.js
+++ b/services/david/david.tester.js
@@ -14,65 +14,55 @@ const t = (module.exports = new ServiceTester({ id: 'david', title: 'David' }))
 
 t.create('david dependencies (valid)')
   .get('/expressjs/express.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: isDependencyStatus,
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: isDependencyStatus,
+  })
 
 t.create('david dev dependencies (valid)')
   .get('/dev/expressjs/express.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dev dependencies',
-      value: isDependencyStatus,
-    })
-  )
+  .expectBadge({
+    label: 'dev dependencies',
+    message: isDependencyStatus,
+  })
 
 t.create('david optional dependencies (valid)')
   .get('/optional/elnounch/byebye.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'optional dependencies',
-      value: isDependencyStatus,
-    })
-  )
+  .expectBadge({
+    label: 'optional dependencies',
+    message: isDependencyStatus,
+  })
 
 t.create('david peer dependencies (valid)')
   .get('/peer/webcomponents/generator-element.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'peer dependencies',
-      value: isDependencyStatus,
-    })
-  )
+  .expectBadge({
+    label: 'peer dependencies',
+    message: isDependencyStatus,
+  })
 
 t.create('david dependencies with path (valid)')
   .get('/babel/babel.json?path=packages/babel-core')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: isDependencyStatus,
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: isDependencyStatus,
+  })
 
 t.create('david dependencies (none)')
   .get('/peer/expressjs/express.json') // express does not specify peer dependencies
-  .expectJSON({ name: 'peer dependencies', value: 'none' })
+  .expectBadge({ label: 'peer dependencies', message: 'none' })
 
 t.create('david dependencies (repo not found)')
   .get('/pyvesb/emptyrepo.json')
-  .expectJSON({ name: 'dependencies', value: 'invalid' })
+  .expectBadge({ label: 'dependencies', message: 'invalid' })
 
 t.create('david dependencies (path not found')
   .get('/babel/babel.json?path=invalid/path')
-  .expectJSON({ name: 'dependencies', value: 'invalid' })
+  .expectBadge({ label: 'dependencies', message: 'invalid' })
 
 t.create('david dependencies (connection error)')
   .get('/expressjs/express.json')
   .networkOff()
-  .expectJSON({ name: 'dependencies', value: 'inaccessible' })
+  .expectBadge({ label: 'dependencies', message: 'inaccessible' })
 
 t.create('david dependencies (unexpected response)')
   .get('/expressjs/express.json')
@@ -81,4 +71,4 @@ t.create('david dependencies (unexpected response)')
       .get('/expressjs/express/info.json')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'dependencies', value: 'invalid' })
+  .expectBadge({ label: 'dependencies', message: 'invalid' })
diff --git a/services/debug/debug.tester.js b/services/debug/debug.tester.js
index efb0cfa086820ae0859235aa179185dc17683402..2885b9479d1852a352d1a7570f8cceb1e8e0c176 100644
--- a/services/debug/debug.tester.js
+++ b/services/debug/debug.tester.js
@@ -5,12 +5,12 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('start time')
   .get('/starttime.json')
-  .expectJSONTypes({ name: 'start time', value: Joi.date().required() })
+  .expectBadge({ label: 'start time', message: Joi.date().required() })
 
 t.create('Flip: first request')
   .get('/flip.json')
-  .expectJSON({ name: 'flip', value: 'on' })
+  .expectBadge({ label: 'flip', message: 'on' })
 
 t.create('Flip: second request')
   .get('/flip.json')
-  .expectJSON({ name: 'flip', value: 'off' })
+  .expectBadge({ label: 'flip', message: 'off' })
diff --git a/services/dependabot/dependabot.tester.js b/services/dependabot/dependabot.tester.js
index 7aca941740a81d6251f2b56d0750e95de9928405..ebdb990e675a93f2abc49172e74e0630a063c749 100644
--- a/services/dependabot/dependabot.tester.js
+++ b/services/dependabot/dependabot.tester.js
@@ -1,31 +1,26 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('semver stability (valid)')
   .get('/bundler/puma.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'semver stability',
-      value: isIntegerPercentage,
-      link:
-        'https://dependabot.com/compatibility-score.html?dependency-name=puma&package-manager=bundler&version-scheme=semver',
-    })
-  )
+  .expectBadge({
+    label: 'semver stability',
+    message: isIntegerPercentage,
+  })
 
 t.create('semver stability (invalid error)')
   .get('/invalid-manager/puma.json?style=_shields_test')
-  .expectJSON({
-    name: 'semver stability',
-    value: 'invalid',
+  .expectBadge({
+    label: 'semver stability',
+    message: 'invalid',
     color: 'lightgrey',
   })
 
 t.create('semver stability (missing dependency)')
   .get('/bundler/some-random-missing-dependency.json')
-  .expectJSON({
-    name: 'semver stability',
-    value: 'unknown',
+  .expectBadge({
+    label: 'semver stability',
+    message: 'unknown',
   })
diff --git a/services/depfu/depfu.tester.js b/services/depfu/depfu.tester.js
index c72b117efc5d2235de48504f596b7082105650f7..957704f3c47cbe4072e801402d068bbdea2efe35 100644
--- a/services/depfu/depfu.tester.js
+++ b/services/depfu/depfu.tester.js
@@ -14,13 +14,11 @@ const t = (module.exports = new ServiceTester({ id: 'depfu', title: 'Depfu' }))
 
 t.create('depfu dependencies (valid)')
   .get('/depfu/example-ruby.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: isDependencyStatus,
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: isDependencyStatus,
+  })
 
 t.create('depfu dependencies (repo not found)')
   .get('/pyvesb/emptyrepo.json')
-  .expectJSON({ name: 'dependencies', value: 'not found' })
+  .expectBadge({ label: 'dependencies', message: 'not found' })
diff --git a/services/discord/discord.tester.js b/services/discord/discord.tester.js
index d4da5cd918626a2c747dde6207ebcc58eb911f1a..102fdef3e5a67f3669a8bf417b7714ed84009cc9 100644
--- a/services/discord/discord.tester.js
+++ b/services/discord/discord.tester.js
@@ -5,17 +5,15 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('gets status for Reactiflux')
   .get('/102860784329052160.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'chat',
-      value: Joi.string().regex(/^[0-9]+ online$/),
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'chat',
+    message: Joi.string().regex(/^[0-9]+ online$/),
+    color: 'brightgreen',
+  })
 
 t.create('invalid server ID')
   .get('/12345.json')
-  .expectJSON({ name: 'chat', value: 'invalid server' })
+  .expectBadge({ label: 'chat', message: 'invalid server' })
 
 t.create('widget disabled')
   .get('/12345.json')
@@ -27,7 +25,7 @@ t.create('widget disabled')
         message: 'Widget Disabled',
       })
   )
-  .expectJSON({ name: 'chat', value: 'widget disabled' })
+  .expectBadge({ label: 'chat', message: 'widget disabled' })
 
 t.create('server error')
   .get('/12345.json')
@@ -36,4 +34,4 @@ t.create('server error')
       .get('/api/guilds/12345/widget.json')
       .reply(500, 'Something broke')
   )
-  .expectJSON({ name: 'chat', value: 'inaccessible' })
+  .expectBadge({ label: 'chat', message: 'inaccessible' })
diff --git a/services/discourse/discourse.tester.js b/services/discourse/discourse.tester.js
index 2840dc95adf263cdb6ca7221e69bbda64066fd39..39ef838eda55974df78550fce89483df7f5a1f95 100644
--- a/services/discourse/discourse.tester.js
+++ b/services/discourse/discourse.tester.js
@@ -32,7 +32,7 @@ t.create('Topics')
       .get('/site/statistics.json')
       .reply(200, data)
   )
-  .expectJSON({ name: 'discourse', value: '23k topics' })
+  .expectBadge({ label: 'discourse', message: '23k topics' })
 
 t.create('Posts')
   .get('/https/meta.discourse.org/posts.json')
@@ -41,7 +41,7 @@ t.create('Posts')
       .get('/site/statistics.json')
       .reply(200, data)
   )
-  .expectJSON({ name: 'discourse', value: '338k posts' })
+  .expectBadge({ label: 'discourse', message: '338k posts' })
 
 t.create('Users')
   .get('/https/meta.discourse.org/users.json')
@@ -50,7 +50,7 @@ t.create('Users')
       .get('/site/statistics.json')
       .reply(200, data)
   )
-  .expectJSON({ name: 'discourse', value: '31k users' })
+  .expectBadge({ label: 'discourse', message: '31k users' })
 
 t.create('Likes')
   .get('/https/meta.discourse.org/likes.json')
@@ -59,7 +59,7 @@ t.create('Likes')
       .get('/site/statistics.json')
       .reply(200, data)
   )
-  .expectJSON({ name: 'discourse', value: '309k likes' })
+  .expectBadge({ label: 'discourse', message: '309k likes' })
 
 t.create('Status')
   .get('/https/meta.discourse.org/status.json')
@@ -68,7 +68,7 @@ t.create('Status')
       .get('/site/statistics.json')
       .reply(200, data)
   )
-  .expectJSON({ name: 'discourse', value: 'online' })
+  .expectBadge({ label: 'discourse', message: 'online' })
 
 t.create('Status with http (not https)')
   .get('/http/meta.discourse.org/status.json')
@@ -77,12 +77,12 @@ t.create('Status with http (not https)')
       .get('/site/statistics.json')
       .reply(200, data)
   )
-  .expectJSON({ name: 'discourse', value: 'online' })
+  .expectBadge({ label: 'discourse', message: 'online' })
 
 t.create('Status (offline)')
   .get('/https/meta.discourse.org/status.json')
   .networkOff()
-  .expectJSON({ name: 'discourse', value: 'inaccessible' })
+  .expectBadge({ label: 'discourse', message: 'inaccessible' })
 
 t.create('Invalid Host')
   .get('/https/some.host/status.json')
@@ -91,44 +91,36 @@ t.create('Invalid Host')
       .get('/site/statistics.json')
       .reply(404, '<h1>Not Found</h1>')
   )
-  .expectJSON({ name: 'discourse', value: 'not found' })
+  .expectBadge({ label: 'discourse', message: 'not found' })
 
 t.create('Topics (live)')
   .get('/https/meta.discourse.org/topics.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'discourse',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? topics$/),
-    })
-  )
+  .expectBadge({
+    label: 'discourse',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? topics$/),
+  })
 
 t.create('Posts (live)')
   .get('/https/meta.discourse.org/posts.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'discourse',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? posts$/),
-    })
-  )
+  .expectBadge({
+    label: 'discourse',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? posts$/),
+  })
 
 t.create('Users (live)')
   .get('/https/meta.discourse.org/users.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'discourse',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? users$/),
-    })
-  )
+  .expectBadge({
+    label: 'discourse',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? users$/),
+  })
 
 t.create('Likes (live)')
   .get('/https/meta.discourse.org/likes.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'discourse',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? likes$/),
-    })
-  )
+  .expectBadge({
+    label: 'discourse',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? likes$/),
+  })
 
 t.create('Status (live)')
   .get('/https/meta.discourse.org/status.json')
-  .expectJSON({ name: 'discourse', value: 'online' })
+  .expectBadge({ label: 'discourse', message: 'online' })
diff --git a/services/dockbit/dockbit.tester.js b/services/dockbit/dockbit.tester.js
index 3c1f69a910db1b783d95121ea7067bd0187fef0b..4d9ef389a832183154ce2846a3bd813cb64cb321 100644
--- a/services/dockbit/dockbit.tester.js
+++ b/services/dockbit/dockbit.tester.js
@@ -9,14 +9,14 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (previously image size)')
   .get('/image-size/_/ubuntu/latest.json')
-  .expectJSON({
-    name: 'dockbit',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'dockbit',
+    message: 'no longer available',
   })
 
 t.create('no longer available (previously number of layers)')
   .get('/layers/_/ubuntu/latest.json')
-  .expectJSON({
-    name: 'dockbit',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'dockbit',
+    message: 'no longer available',
   })
diff --git a/services/docker/docker-automated.tester.js b/services/docker/docker-automated.tester.js
index 48490c7ba43a56028e1e2a8fd8cb5e8291e9d9b2..ce26dc8ef27816f04f852c00b3a3f19182fbfb93 100644
--- a/services/docker/docker-automated.tester.js
+++ b/services/docker/docker-automated.tester.js
@@ -8,25 +8,21 @@ const isAutomatedBuildStatus = Joi.string().valid('automated', 'manual')
 
 t.create('docker automated build (valid, library)')
   .get('/_/ubuntu.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker build',
-      value: isAutomatedBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'docker build',
+    message: isAutomatedBuildStatus,
+  })
 
 t.create('docker automated build (valid, user)')
   .get('/jrottenberg/ffmpeg.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker build',
-      value: isAutomatedBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'docker build',
+    message: isAutomatedBuildStatus,
+  })
 
 t.create('docker automated build (not found)')
   .get('/_/not-a-real-repo.json')
-  .expectJSON({ name: 'docker build', value: 'repo not found' })
+  .expectBadge({ label: 'docker build', message: 'repo not found' })
 
 t.create('docker automated build - automated')
   .get('/_/ubuntu.json?style=_shields_test')
@@ -35,9 +31,9 @@ t.create('docker automated build - automated')
       .get('/v2/repositories/library/ubuntu')
       .reply(200, { is_automated: true })
   )
-  .expectJSON({
-    name: 'docker build',
-    value: 'automated',
+  .expectBadge({
+    label: 'docker build',
+    message: 'automated',
     color: `#${dockerBlue}`,
   })
 
@@ -48,4 +44,4 @@ t.create('docker automated build - manual')
       .get('/v2/repositories/library/ubuntu')
       .reply(200, { is_automated: false })
   )
-  .expectJSON({ name: 'docker build', value: 'manual', color: 'yellow' })
+  .expectBadge({ label: 'docker build', message: 'manual', color: 'yellow' })
diff --git a/services/docker/docker-build.tester.js b/services/docker/docker-build.tester.js
index 591cea5c4564a3dfac56442a142bd2fa80980a1c..500ea62ca01a4784ace75bd728f4755a728d421f 100644
--- a/services/docker/docker-build.tester.js
+++ b/services/docker/docker-build.tester.js
@@ -1,22 +1,19 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const t = (module.exports = require('../tester').createServiceTester())
 const { dockerBlue } = require('./docker-helpers')
 
 t.create('docker build status (valid, user)')
   .get('/jrottenberg/ffmpeg.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'docker build',
+    message: isBuildStatus,
+  })
 
 t.create('docker build status (not found)')
   .get('/_/not-a-real-repo.json')
-  .expectJSON({ name: 'docker build', value: 'repo not found' })
+  .expectBadge({ label: 'docker build', message: 'repo not found' })
 
 t.create('docker build status (passing)')
   .get('/_/ubuntu.json?style=_shields_test')
@@ -25,9 +22,9 @@ t.create('docker build status (passing)')
       .get('/v2/repositories/library/ubuntu/buildhistory')
       .reply(200, { results: [{ status: 10 }] })
   )
-  .expectJSON({
-    name: 'docker build',
-    value: 'passing',
+  .expectBadge({
+    label: 'docker build',
+    message: 'passing',
     color: 'brightgreen',
   })
 
@@ -38,7 +35,7 @@ t.create('docker build status (failing)')
       .get('/v2/repositories/library/ubuntu/buildhistory')
       .reply(200, { results: [{ status: -1 }] })
   )
-  .expectJSON({ name: 'docker build', value: 'failing', color: 'red' })
+  .expectBadge({ label: 'docker build', message: 'failing', color: 'red' })
 
 t.create('docker build status (building)')
   .get('/_/ubuntu.json?style=_shields_test')
@@ -47,8 +44,8 @@ t.create('docker build status (building)')
       .get('/v2/repositories/library/ubuntu/buildhistory')
       .reply(200, { results: [{ status: 1 }] })
   )
-  .expectJSON({
-    name: 'docker build',
-    value: 'building',
+  .expectBadge({
+    label: 'docker build',
+    message: 'building',
     color: `#${dockerBlue}`,
   })
diff --git a/services/docker/docker-pulls.tester.js b/services/docker/docker-pulls.tester.js
index 0b29ca0ce72a60d9b199638b7422d4ac5fd9e6c9..46750aae268423b0cd8d56f766c91b0fad356c97 100644
--- a/services/docker/docker-pulls.tester.js
+++ b/services/docker/docker-pulls.tester.js
@@ -1,29 +1,24 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 const { dockerBlue } = require('./docker-helpers')
 
 t.create('docker pulls (valid, library)')
   .get('/_/ubuntu.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker pulls',
-      value: isMetric,
-      color: `#${dockerBlue}`,
-    })
-  )
+  .expectBadge({
+    label: 'docker pulls',
+    message: isMetric,
+    color: `#${dockerBlue}`,
+  })
 
 t.create('docker pulls (valid, user)')
   .get('/jrottenberg/ffmpeg.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker pulls',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'docker pulls',
+    message: isMetric,
+  })
 
 t.create('docker pulls (not found)')
   .get('/_/not-a-real-repo.json')
-  .expectJSON({ name: 'docker pulls', value: 'repo not found' })
+  .expectBadge({ label: 'docker pulls', message: 'repo not found' })
diff --git a/services/docker/docker-stars.tester.js b/services/docker/docker-stars.tester.js
index d3a4b57fa5d6fd26f170f4bf02c734aff2c2d0fd..2d41dea5dcb0ecb11f8434aca80d85818c48d83f 100644
--- a/services/docker/docker-stars.tester.js
+++ b/services/docker/docker-stars.tester.js
@@ -1,29 +1,24 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 const { dockerBlue } = require('./docker-helpers')
 
 t.create('docker stars (valid, library)')
   .get('/_/ubuntu.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker stars',
-      value: isMetric,
-      color: `#${dockerBlue}`,
-    })
-  )
+  .expectBadge({
+    label: 'docker stars',
+    message: isMetric,
+    color: `#${dockerBlue}`,
+  })
 
 t.create('docker stars (valid, user)')
   .get('/jrottenberg/ffmpeg.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docker stars',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'docker stars',
+    message: isMetric,
+  })
 
 t.create('docker stars (not found)')
   .get('/_/not-a-real-repo.json')
-  .expectJSON({ name: 'docker stars', value: 'repo not found' })
+  .expectBadge({ label: 'docker stars', message: 'repo not found' })
diff --git a/services/dotnetstatus/dotnetstatus.tester.js b/services/dotnetstatus/dotnetstatus.tester.js
index 745770954f35efc9d8bc47a0f99964951f548b15..e2b0ea1b335202611360b20dfa0fd97e12332fa6 100644
--- a/services/dotnetstatus/dotnetstatus.tester.js
+++ b/services/dotnetstatus/dotnetstatus.tester.js
@@ -9,7 +9,7 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (previously get package status)')
   .get('/gh/jaredcnance/dotnet-status/API.json')
-  .expectJSON({
-    name: 'dotnet status',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'dotnet status',
+    message: 'no longer available',
   })
diff --git a/services/dub/dub-download.tester.js b/services/dub/dub-download.tester.js
index aa27f8a83324c7c0044e7bad1873e783561c09e2..f76a3cf62554135c2ecd3e1d7b97fe66ac75490c 100644
--- a/services/dub/dub-download.tester.js
+++ b/services/dub/dub-download.tester.js
@@ -19,65 +19,53 @@ const t = (module.exports = new ServiceTester({
 
 t.create('total downloads (valid)')
   .get('/dt/vibe-d.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-      color: isDownloadsColor,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+    color: isDownloadsColor,
+  })
 
 t.create('total downloads, specific version (valid)')
   .get('/dt/vibe-d/0.8.4.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads@0.8.4',
-      value: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]?$/),
-      color: isDownloadsColor,
-    })
-  )
+  .expectBadge({
+    label: 'downloads@0.8.4',
+    message: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]?$/),
+    color: isDownloadsColor,
+  })
   .timeout(15000)
 
 t.create('total downloads, latest version (valid)')
   .get('/dt/vibe-d/latest.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads@latest',
-      value: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]?$/),
-      color: isDownloadsColor,
-    })
-  )
+  .expectBadge({
+    label: 'downloads@latest',
+    message: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]?$/),
+    color: isDownloadsColor,
+  })
 
 t.create('daily downloads (valid)')
   .get('/dd/vibe-d.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-      color: isDownloadsColor,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+    color: isDownloadsColor,
+  })
 
 t.create('weekly downloads (valid)')
   .get('/dw/vibe-d.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-      color: isDownloadsColor,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+    color: isDownloadsColor,
+  })
 
 t.create('monthly downloads (valid)')
   .get('/dm/vibe-d.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-      color: isDownloadsColor,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+    color: isDownloadsColor,
+  })
 
 t.create('total downloads (not found)')
   .get('/dt/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
diff --git a/services/dub/dub-license.tester.js b/services/dub/dub-license.tester.js
index 357b58b79540e35557d4e6583195eed81dc1dac3..f40797ac530f0b8eddfab1c9f8e67f664e6919b0 100644
--- a/services/dub/dub-license.tester.js
+++ b/services/dub/dub-license.tester.js
@@ -4,8 +4,8 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('license (valid)')
   .get('/vibe-d.json')
-  .expectJSON({ name: 'license', value: 'MIT' })
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 t.create('license (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'license', value: 'not found' })
+  .expectBadge({ label: 'license', message: 'not found' })
diff --git a/services/dub/dub-version.tester.js b/services/dub/dub-version.tester.js
index 81f045a1ed50276b7437d69f7ec92948d36c7200..0ef53a7b34291d81df2f808fe565af0c6fc7794d 100644
--- a/services/dub/dub-version.tester.js
+++ b/services/dub/dub-version.tester.js
@@ -8,14 +8,12 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('version (valid)')
   .get('/vibe-d.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dub',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-      color: Joi.equal('blue', 'orange'),
-    })
-  )
+  .expectBadge({
+    label: 'dub',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+    color: Joi.equal('blue', 'orange'),
+  })
 
 t.create('version (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'dub', value: 'not found' })
+  .expectBadge({ label: 'dub', message: 'not found' })
diff --git a/services/dynamic/dynamic-json.tester.js b/services/dynamic/dynamic-json.tester.js
index 582b07986fa0c7c8d5a2bdaf3c5e4343ccacb929..0d507f15a350b93d4bc7f826eb60a5bb82481b02 100644
--- a/services/dynamic/dynamic-json.tester.js
+++ b/services/dynamic/dynamic-json.tester.js
@@ -6,9 +6,9 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('No URL specified')
   .get('.json?query=$.name&label=Package Name&style=_shields_test')
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid query parameter: url',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid query parameter: url',
     color: 'red',
   })
 
@@ -16,9 +16,9 @@ t.create('No query specified')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&label=Package Name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid query parameter: query',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid query parameter: query',
     color: 'red',
   })
 
@@ -26,9 +26,9 @@ t.create('Malformed url')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/%0package.json&query=$.name&label=Package Name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid',
     color: 'lightgrey',
   })
 
@@ -36,9 +36,9 @@ t.create('JSON from url')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&query=$.name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'shields.io',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'shields.io',
     color: 'blue',
   })
 
@@ -46,9 +46,9 @@ t.create('JSON from uri (support uri query parameter)')
   .get(
     '.json?uri=https://github.com/badges/shields/raw/master/package.json&query=$.name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'shields.io',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'shields.io',
     color: 'blue',
   })
 
@@ -56,37 +56,33 @@ t.create('JSON from url | multiple results')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&query=$..keywords[0:2:1]'
   )
-  .expectJSON({ name: 'custom badge', value: 'GitHub, badge' })
+  .expectBadge({ label: 'custom badge', message: 'GitHub, badge' })
 
 t.create('JSON from url | caching with new query params')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&query=$.version'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'custom badge',
-      value: Joi.string().regex(/^\d+(\.\d+)?(\.\d+)?$/),
-    })
-  )
+  .expectBadge({
+    label: 'custom badge',
+    message: Joi.string().regex(/^\d+(\.\d+)?(\.\d+)?$/),
+  })
 
 t.create('JSON from url | with prefix & suffix & label')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&query=$.version&prefix=v&suffix= dev&label=Shields'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'Shields',
-      value: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)?\sdev$/),
-    })
-  )
+  .expectBadge({
+    label: 'Shields',
+    message: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)?\sdev$/),
+  })
 
 t.create('JSON from url | object doesnt exist')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&query=$.does_not_exist&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'no result',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'no result',
     color: 'lightgrey',
   })
 
@@ -94,9 +90,9 @@ t.create('JSON from url | invalid url')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/notafile.json&query=$.version&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'resource not found',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'resource not found',
     color: 'red',
   })
 
@@ -104,23 +100,27 @@ t.create('JSON from url | user color overrides default')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/package.json&query=$.name&color=10ADED&style=_shields_test'
   )
-  .expectJSON({ name: 'custom badge', value: 'shields.io', color: '#10aded' })
+  .expectBadge({
+    label: 'custom badge',
+    message: 'shields.io',
+    color: '#10aded',
+  })
 
 t.create('JSON from url | error color overrides default')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/notafile.json&query=$.version&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'resource not found',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'resource not found',
     color: 'red',
   })
 
 t.create('JSON from url | error color overrides user specified')
   .get('.json?query=$.version&color=10ADED&style=_shields_test')
-  .expectJSON({
-    name: 'custom badge',
-    value: 'invalid query parameter: url',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'invalid query parameter: url',
     color: 'red',
   })
 
@@ -135,7 +135,7 @@ t.create('JSON from url | request should set Accept header')
         return '{"name":"test"}'
       })
   )
-  .expectJSON({ name: 'custom badge', value: 'test' })
+  .expectBadge({ label: 'custom badge', message: 'test' })
   .after(() => {
     expect(headers).to.have.property('accept', 'application/json')
   })
diff --git a/services/dynamic/dynamic-xml.tester.js b/services/dynamic/dynamic-xml.tester.js
index ef1bc846d499519dec26ac1dc596f357817d039f..b1d9bb9a3b6c36b7c1cadd5f7f0d5fe069d2fcf8 100644
--- a/services/dynamic/dynamic-xml.tester.js
+++ b/services/dynamic/dynamic-xml.tester.js
@@ -7,9 +7,9 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('No URL specified')
   .get('.json?query=//name&label=Package Name&style=_shields_test')
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid query parameter: url',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid query parameter: url',
     color: 'red',
   })
 
@@ -17,9 +17,9 @@ t.create('No query specified')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&label=Package Name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid query parameter: query',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid query parameter: query',
     color: 'red',
   })
 
@@ -27,9 +27,9 @@ t.create('XML from url')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/addon/name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'IndieGala Helper',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'IndieGala Helper',
     color: 'blue',
   })
 
@@ -37,9 +37,9 @@ t.create('XML from uri (support uri query parameter)')
   .get(
     '.json?uri=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/addon/name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'IndieGala Helper',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'IndieGala Helper',
     color: 'blue',
   })
 
@@ -47,55 +47,47 @@ t.create('XML from url (attribute)')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/addon/reviews/@num'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'custom badge',
-      value: Joi.string().regex(/^\d+$/),
-    })
-  )
+  .expectBadge({
+    label: 'custom badge',
+    message: Joi.string().regex(/^\d+$/),
+  })
 
 t.create('XML from url | multiple results')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/addon/compatible_applications/application/name'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'custom badge',
-      value: Joi.string().regex(
-        /^Firefox( for Android)?,\sFirefox( for Android)?$/
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'custom badge',
+    message: Joi.string().regex(
+      /^Firefox( for Android)?,\sFirefox( for Android)?$/
+    ),
+  })
 
 t.create('XML from url | caching with new query params')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/addon/version'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'custom badge',
-      value: isSemver,
-    })
-  )
+  .expectBadge({
+    label: 'custom badge',
+    message: isSemver,
+  })
 
 t.create('XML from url | with prefix & suffix & label')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=//version&prefix=v&suffix= dev&label=IndieGala Helper'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'IndieGala Helper',
-      value: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)?\sdev$/),
-    })
-  )
+  .expectBadge({
+    label: 'IndieGala Helper',
+    message: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)?\sdev$/),
+  })
 
 t.create('XML from url | query doesnt exist')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/does/not/exist&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'no result',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'no result',
     color: 'lightgrey',
   })
 
@@ -103,9 +95,9 @@ t.create('XML from url | query doesnt exist (attribute)')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/does/not/@exist&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'no result',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'no result',
     color: 'lightgrey',
   })
 
@@ -113,9 +105,9 @@ t.create('XML from url | invalid url')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/notafile.xml&query=//version&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'resource not found',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'resource not found',
     color: 'red',
   })
 
@@ -123,9 +115,9 @@ t.create('XML from url | user color overrides default')
   .get(
     '.json?url=https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/707078&query=/addon/name&color=10ADED&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'IndieGala Helper',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'IndieGala Helper',
     color: '#10aded',
   })
 
@@ -133,17 +125,17 @@ t.create('XML from url | error color overrides default')
   .get(
     '.json?url=https://github.com/badges/shields/raw/master/notafile.xml&query=//version&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'resource not found',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'resource not found',
     color: 'red',
   })
 
 t.create('XML from url | error color overrides user specified')
   .get('.json?query=//version&color=10ADED&style=_shields_test')
-  .expectJSON({
-    name: 'custom badge',
-    value: 'invalid query parameter: url',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'invalid query parameter: url',
     color: 'red',
   })
 
@@ -158,7 +150,7 @@ t.create('XML from url | request should set Accept header')
         return '<?xml version="1.0" encoding="utf-8" ?><name>dynamic xml</name>'
       })
   )
-  .expectJSON({ name: 'custom badge', value: 'dynamic xml' })
+  .expectBadge({ label: 'custom badge', message: 'dynamic xml' })
   .after(() => {
     expect(headers).to.have.property('accept', 'application/xml, text/xml')
   })
diff --git a/services/dynamic/dynamic-yaml.tester.js b/services/dynamic/dynamic-yaml.tester.js
index 671bcc0e859e4e7b1e167b6625eb4a63551d7d84..d2a5c2c23e8a18e92d0e160ab276e97ec1d8b4fa 100644
--- a/services/dynamic/dynamic-yaml.tester.js
+++ b/services/dynamic/dynamic-yaml.tester.js
@@ -4,9 +4,9 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('No URL specified')
   .get('.json?query=$.name&label=Package Name&style=_shields_test')
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid query parameter: url',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid query parameter: url',
     color: 'red',
   })
 
@@ -14,9 +14,9 @@ t.create('No query specified')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&label=Package Name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'Package Name',
-    value: 'invalid query parameter: query',
+  .expectBadge({
+    label: 'Package Name',
+    message: 'invalid query parameter: query',
     color: 'red',
   })
 
@@ -24,9 +24,9 @@ t.create('YAML from url')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$.name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'coredns',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'coredns',
     color: 'blue',
   })
 
@@ -34,9 +34,9 @@ t.create('YAML from uri (support uri query parameter)')
   .get(
     '.json?uri=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$.name&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'coredns',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'coredns',
     color: 'blue',
   })
 
@@ -44,27 +44,27 @@ t.create('YAML from url | multiple results')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$..keywords[0:2:1]'
   )
-  .expectJSON({ name: 'custom badge', value: 'coredns, dns' })
+  .expectBadge({ label: 'custom badge', message: 'coredns, dns' })
 
 t.create('YAML from url | caching with new query params')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$.version'
   )
-  .expectJSON({ name: 'custom badge', value: '0.8.0' })
+  .expectBadge({ label: 'custom badge', message: '0.8.0' })
 
 t.create('YAML from url | with prefix & suffix & label')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$.version&prefix=v&suffix= dev&label=Shields'
   )
-  .expectJSON({ name: 'Shields', value: 'v0.8.0 dev' })
+  .expectBadge({ label: 'Shields', message: 'v0.8.0 dev' })
 
 t.create('YAML from url | object doesnt exist')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$.does_not_exist&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'no result',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'no result',
     color: 'lightgrey',
   })
 
@@ -72,9 +72,9 @@ t.create('YAML from url | invalid url')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/notafile.yaml&query=$.version&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'resource not found',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'resource not found',
     color: 'red',
   })
 
@@ -82,22 +82,22 @@ t.create('YAML from url | user color overrides default')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/Chart.yaml&query=$.name&color=10ADED&style=_shields_test'
   )
-  .expectJSON({ name: 'custom badge', value: 'coredns', color: '#10aded' })
+  .expectBadge({ label: 'custom badge', message: 'coredns', color: '#10aded' })
 
 t.create('YAML from url | error color overrides default')
   .get(
     '.json?url=https://raw.githubusercontent.com/kubernetes/charts/568291d6e476c39ca8322c30c3f601d0383d4760/stable/coredns/notafile.yaml&query=$.version&style=_shields_test'
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'resource not found',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'resource not found',
     color: 'red',
   })
 
 t.create('YAML from url | error color overrides user specified')
   .get('.json?query=$.version&color=10ADED&style=_shields_test')
-  .expectJSON({
-    name: 'custom badge',
-    value: 'invalid query parameter: url',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'invalid query parameter: url',
     color: 'red',
   })
diff --git a/services/eclipse-marketplace/eclipse-marketplace-downloads.tester.js b/services/eclipse-marketplace/eclipse-marketplace-downloads.tester.js
index d2f6724150e958b4bf6be4295d7215f028a6b97c..42a4fd17716d3236e5994bb7d8c8af7c1f9f55d7 100644
--- a/services/eclipse-marketplace/eclipse-marketplace-downloads.tester.js
+++ b/services/eclipse-marketplace/eclipse-marketplace-downloads.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric, isMetricOverTimePeriod } = require('../test-validators')
 
@@ -12,25 +11,21 @@ const t = (module.exports = new ServiceTester({
 
 t.create('total marketplace downloads')
   .get('/dt/notepad4e.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('monthly marketplace downloads')
   .get('/dm/notepad4e.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('downloads for unknown solution')
   .get('/dt/this-does-not-exist.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'solution not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'solution not found',
   })
diff --git a/services/eclipse-marketplace/eclipse-marketplace-favorites.tester.js b/services/eclipse-marketplace/eclipse-marketplace-favorites.tester.js
index f8aba22ef273d24b081cae11cea39f9b8674697c..3a3c9070e81afb54622ad9e3020a1cd29753b7aa 100644
--- a/services/eclipse-marketplace/eclipse-marketplace-favorites.tester.js
+++ b/services/eclipse-marketplace/eclipse-marketplace-favorites.tester.js
@@ -5,18 +5,16 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('favorites count')
   .get('/notepad4e.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'favorites',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'favorites',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('favorites for unknown solution')
   .get('/this-does-not-exist.json')
-  .expectJSON({
-    name: 'favorites',
-    value: 'solution not found',
+  .expectBadge({
+    label: 'favorites',
+    message: 'solution not found',
   })
diff --git a/services/eclipse-marketplace/eclipse-marketplace-license.tester.js b/services/eclipse-marketplace/eclipse-marketplace-license.tester.js
index 92bf92d827ba0d234d24aa8366f658292a3c01da..129e8b3c34e8f508c0d584e1c7063773befffb39 100644
--- a/services/eclipse-marketplace/eclipse-marketplace-license.tester.js
+++ b/services/eclipse-marketplace/eclipse-marketplace-license.tester.js
@@ -4,9 +4,9 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('license')
   .get('/notepad4e.json')
-  .expectJSON({
-    name: 'license',
-    value: 'GPL',
+  .expectBadge({
+    label: 'license',
+    message: 'GPL',
   })
 
 t.create('unspecified license')
@@ -23,14 +23,14 @@ t.create('unspecified license')
          </marketplace>`
       )
   )
-  .expectJSON({
-    name: 'license',
-    value: 'not specified',
+  .expectBadge({
+    label: 'license',
+    message: 'not specified',
   })
 
 t.create('license for unknown solution')
   .get('/this-does-not-exist.json')
-  .expectJSON({
-    name: 'license',
-    value: 'solution not found',
+  .expectBadge({
+    label: 'license',
+    message: 'solution not found',
   })
diff --git a/services/eclipse-marketplace/eclipse-marketplace-update.tester.js b/services/eclipse-marketplace/eclipse-marketplace-update.tester.js
index c33bdc44e9cea970c442b56117d8239df2ce1b81..b59fc6b2493de07d76c7cfcee0c07791ab6b0b85 100644
--- a/services/eclipse-marketplace/eclipse-marketplace-update.tester.js
+++ b/services/eclipse-marketplace/eclipse-marketplace-update.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFormattedDate } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('last update date')
   .get('/notepad4e.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'updated',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'updated',
+    message: isFormattedDate,
+  })
 
 t.create('last update for unknown solution')
   .get('/this-does-not-exist.json')
-  .expectJSON({
-    name: 'updated',
-    value: 'solution not found',
+  .expectBadge({
+    label: 'updated',
+    message: 'solution not found',
   })
diff --git a/services/eclipse-marketplace/eclipse-marketplace-version.tester.js b/services/eclipse-marketplace/eclipse-marketplace-version.tester.js
index d5c29f822f7b5196cdb2e20f0eed3001483a0091..863c6863e52704a6ad3347325e2e91701b981e9a 100644
--- a/services/eclipse-marketplace/eclipse-marketplace-version.tester.js
+++ b/services/eclipse-marketplace/eclipse-marketplace-version.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('marketplace version')
   .get('/notepad4e.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'eclipse marketplace',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'eclipse marketplace',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('last update for unknown solution')
   .get('/this-does-not-exist.json')
-  .expectJSON({
-    name: 'eclipse marketplace',
-    value: 'solution not found',
+  .expectBadge({
+    label: 'eclipse marketplace',
+    message: 'solution not found',
   })
diff --git a/services/elm-package/elm-package.tester.js b/services/elm-package/elm-package.tester.js
index 4b8ba59399541fbe4d60a0c1f0aecb367ac26b08..d432de124705f6f36f5a7f512647391420d4fab5 100644
--- a/services/elm-package/elm-package.tester.js
+++ b/services/elm-package/elm-package.tester.js
@@ -1,13 +1,12 @@
 'use strict'
 
-const Joi = require('joi')
 const { isSemver } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('gets the package version of elm/core')
   .get('/elm/core.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'elm package', value: isSemver }))
+  .expectBadge({ label: 'elm package', message: isSemver })
 
 t.create('invalid package name')
   .get('/elm-community/frodo-is-not-a-package.json')
-  .expectJSON({ name: 'elm package', value: 'package not found' })
+  .expectBadge({ label: 'elm package', message: 'package not found' })
diff --git a/services/endpoint/endpoint.tester.js b/services/endpoint/endpoint.tester.js
index afe210cbf83161133e4ab293d9a874724db9751c..2ec62e635a3763cb34ddca086a2c20e8eed0e5e2 100644
--- a/services/endpoint/endpoint.tester.js
+++ b/services/endpoint/endpoint.tester.js
@@ -15,7 +15,7 @@ t.create('Valid schema (mocked)')
         message: 'yo',
       })
   )
-  .expectJSON({ name: '', value: 'yo' })
+  .expectBadge({ label: '', message: 'yo' })
 
 t.create('color and labelColor')
   .get('.json?url=https://example.com/badge&style=_shields_test')
@@ -50,9 +50,9 @@ t.create('style')
         style: '_shields_test',
       })
   )
-  .expectJSON({
-    name: 'hey',
-    value: 'yo',
+  .expectBadge({
+    label: 'hey',
+    message: 'yo',
     // `color` is only in _shields_test which is being specified by the
     // service, not the request. If the color key is here we know this has
     // worked.
@@ -145,9 +145,9 @@ t.create('Invalid schema (mocked)')
         schemaVersion: -1,
       })
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'invalid properties: schemaVersion',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'invalid properties: schemaVersion',
   })
 
 t.create('Invalid schema (mocked)')
@@ -163,9 +163,9 @@ t.create('Invalid schema (mocked)')
         bogus: true,
       })
   )
-  .expectJSON({
-    name: 'custom badge',
-    value: 'invalid properties: extra, bogus',
+  .expectBadge({
+    label: 'custom badge',
+    message: 'invalid properties: extra, bogus',
   })
 
 t.create('User color overrides success color')
@@ -180,7 +180,7 @@ t.create('User color overrides success color')
         color: 'blue',
       })
   )
-  .expectJSON({ name: '', value: 'yo', color: '#101010' })
+  .expectBadge({ label: '', message: 'yo', color: '#101010' })
 
 t.create('User legacy color overrides success color')
   .get('.json?url=https://example.com/badge&colorB=101010&style=_shields_test')
@@ -194,7 +194,7 @@ t.create('User legacy color overrides success color')
         color: 'blue',
       })
   )
-  .expectJSON({ name: '', value: 'yo', color: '#101010' })
+  .expectBadge({ label: '', message: 'yo', color: '#101010' })
 
 t.create('User color does not override error color')
   .get('.json?url=https://example.com/badge&color=101010&style=_shields_test')
@@ -209,7 +209,7 @@ t.create('User color does not override error color')
         color: 'red',
       })
   )
-  .expectJSON({ name: 'something is', value: 'not right', color: 'red' })
+  .expectBadge({ label: 'something is', message: 'not right', color: 'red' })
 
 t.create('User legacy color does not override error color')
   .get('.json?url=https://example.com/badge&colorB=101010&style=_shields_test')
@@ -224,7 +224,7 @@ t.create('User legacy color does not override error color')
         color: 'red',
       })
   )
-  .expectJSON({ name: 'something is', value: 'not right', color: 'red' })
+  .expectBadge({ label: 'something is', message: 'not right', color: 'red' })
 
 t.create('cacheSeconds')
   .get('.json?url=https://example.com/badge')
@@ -284,8 +284,8 @@ t.create('cacheSeconds does not override longer Shields default')
 
 t.create('Bad scheme')
   .get('.json?url=http://example.com/badge')
-  .expectJSON({ name: 'custom badge', value: 'please use https' })
+  .expectBadge({ label: 'custom badge', message: 'please use https' })
 
 t.create('Blocked domain')
   .get('.json?url=https://img.shields.io/badge/foo-bar-blue.json')
-  .expectJSON({ name: 'custom badge', value: 'domain is blocked' })
+  .expectBadge({ label: 'custom badge', message: 'domain is blocked' })
diff --git a/services/f-droid/f-droid.tester.js b/services/f-droid/f-droid.tester.js
index b636e4ca4d063e2b77822d359ee8fb01db5b3389..dbee13cf3713f7fe519d464cde7c690d729b4c90 100644
--- a/services/f-droid/f-droid.tester.js
+++ b/services/f-droid/f-droid.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 
@@ -114,7 +113,7 @@ t.create('Package is found with default metadata format')
       .get(`${path}.txt`)
       .reply(200, testString)
   )
-  .expectJSON({ name: 'f-droid', value: 'v1.4' })
+  .expectBadge({ label: 'f-droid', message: 'v1.4' })
 
 t.create('Package is found with fallback yml matadata format')
   .get('/v/axp.tool.apkextractor.json')
@@ -128,7 +127,7 @@ t.create('Package is found with fallback yml matadata format')
       .get(`${path}.yml`)
       .reply(200, testYmlString)
   )
-  .expectJSON({ name: 'f-droid', value: 'v1.4' })
+  .expectBadge({ label: 'f-droid', message: 'v1.4' })
 
 t.create('Package is found with yml matadata format')
   .get('/v/axp.tool.apkextractor.json?metadata_format=yml')
@@ -137,7 +136,7 @@ t.create('Package is found with yml matadata format')
       .get(`${path}.yml`)
       .reply(200, testYmlString)
   )
-  .expectJSON({ name: 'f-droid', value: 'v1.4' })
+  .expectBadge({ label: 'f-droid', message: 'v1.4' })
 
 t.create('Package is not found with "metadata_format" query parameter')
   .get('/v/axp.tool.apkextractor.json?metadata_format=yml')
@@ -146,7 +145,7 @@ t.create('Package is not found with "metadata_format" query parameter')
       .get(`${path}.yml`)
       .reply(404)
   )
-  .expectJSON({ name: 'f-droid', value: 'app not found' })
+  .expectBadge({ label: 'f-droid', message: 'app not found' })
 
 t.create('Package is found yml matadata format with missing "CurrentVersion"')
   .get('/v/axp.tool.apkextractor.json?metadata_format=yml')
@@ -155,7 +154,7 @@ t.create('Package is found yml matadata format with missing "CurrentVersion"')
       .get(`${path}.yml`)
       .reply(200, 'Categories: System')
   )
-  .expectJSON({ name: 'f-droid', value: 'invalid response data' })
+  .expectBadge({ label: 'f-droid', message: 'invalid response data' })
 
 t.create('Package is found with bad yml matadata format')
   .get('/v/axp.tool.apkextractor.json?metadata_format=yml')
@@ -164,7 +163,7 @@ t.create('Package is found with bad yml matadata format')
       .get(`${path}.yml`)
       .reply(200, '.CurrentVersion: 1.4')
   )
-  .expectJSON({ name: 'f-droid', value: 'invalid response data' })
+  .expectBadge({ label: 'f-droid', message: 'invalid response data' })
 
 t.create('Package is not found')
   .get('/v/axp.tool.apkextractor.json')
@@ -178,7 +177,7 @@ t.create('Package is not found')
       .get(`${path}.yml`)
       .reply(404)
   )
-  .expectJSON({ name: 'f-droid', value: 'app not found' })
+  .expectBadge({ label: 'f-droid', message: 'app not found' })
 
 t.create('The api changed')
   .get('/v/axp.tool.apkextractor.json?metadata_format=yml')
@@ -187,22 +186,20 @@ t.create('The api changed')
       .get(`${path}.yml`)
       .reply(200, '')
   )
-  .expectJSON({ name: 'f-droid', value: 'invalid response data' })
+  .expectBadge({ label: 'f-droid', message: 'invalid response data' })
 
 t.create('Package is not found due invalid metadata format')
   .get('/v/axp.tool.apkextractor.json?metadata_format=xml')
-  .expectJSON({
-    name: 'f-droid',
-    value: 'invalid query parameter: metadata_format',
+  .expectBadge({
+    label: 'f-droid',
+    message: 'invalid query parameter: metadata_format',
   })
 
 /* If this test fails, either the API has changed or the app was deleted. */
 t.create('The real api did not change')
   .get('/v/org.thosp.yourlocalweather.json')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'f-droid',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'f-droid',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
diff --git a/services/gem/gem-downloads.tester.js b/services/gem/gem-downloads.tester.js
index 4073dafa05f6bc33b36d17ecc14296485dd404f9..c42912ecaaecd5bef5117fa5e0078bf64634c390 100644
--- a/services/gem/gem-downloads.tester.js
+++ b/services/gem/gem-downloads.tester.js
@@ -1,74 +1,65 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('total downloads (valid)')
   .get('/dt/rails.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (not found)')
   .get('/dt/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'gem not found' })
+  .expectBadge({ label: 'downloads', message: 'gem not found' })
 
 t.create('version downloads (valid, stable version)')
   .get('/dv/rails/stable.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads@stable',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads@stable',
+    message: isMetric,
+  })
 
 t.create('version downloads (valid, specific version)')
   .get('/dv/rails/4.1.0.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads@4.1.0',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads@4.1.0',
+    message: isMetric,
+  })
 
 t.create('version downloads (package not found)')
   .get('/dv/not-a-package/4.1.0.json')
-  .expectJSON({ name: 'downloads', value: 'gem not found' })
+  .expectBadge({ label: 'downloads', message: 'gem not found' })
 
 t.create('version downloads (valid package, invalid version)')
   .get('/dv/rails/not-a-version.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'version should be "stable" or valid semver',
+  .expectBadge({
+    label: 'downloads',
+    message: 'version should be "stable" or valid semver',
   })
 
 t.create('version downloads (valid package, version not found)')
   .get('/dv/rails/8.8.8.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'version not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'version not found',
   })
 
 t.create('version downloads (valid package, version not specified)')
   .get('/dv/rails.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'version downloads requires a version',
+  .expectBadge({
+    label: 'downloads',
+    message: 'version downloads requires a version',
   })
 
 t.create('latest version downloads (valid)')
   .get('/dtv/rails.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads@latest',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads@latest',
+    message: isMetric,
+  })
 
 t.create('latest version downloads (not found)')
   .get('/dtv/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'gem not found' })
+  .expectBadge({ label: 'downloads', message: 'gem not found' })
diff --git a/services/gem/gem-owner.tester.js b/services/gem/gem-owner.tester.js
index 8816edeee344267a1252cb6f68005d15f1667bfc..80d1d9286c8d7fbb2caca23af47363ef0269f0aa 100644
--- a/services/gem/gem-owner.tester.js
+++ b/services/gem/gem-owner.tester.js
@@ -5,13 +5,11 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('users (valid)')
   .get('/raphink.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'gems',
-      value: Joi.string().regex(/^[0-9]+$/),
-    })
-  )
+  .expectBadge({
+    label: 'gems',
+    message: Joi.string().regex(/^[0-9]+$/),
+  })
 
 t.create('users (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'gems', value: 'not found' })
+  .expectBadge({ label: 'gems', message: 'not found' })
diff --git a/services/gem/gem-rank.tester.js b/services/gem/gem-rank.tester.js
index ed7546ed80a6d2a8dfb141b00f1baeab0e86a624..6546d889deb57d2d4478bcbada80358e8ce19089 100644
--- a/services/gem/gem-rank.tester.js
+++ b/services/gem/gem-rank.tester.js
@@ -10,25 +10,21 @@ const isOrdinalNumberDaily = Joi.string().regex(
 
 t.create('total rank (valid)')
   .get('/rt/rspec-puppet-facts.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rank',
-      value: isOrdinalNumber,
-    })
-  )
+  .expectBadge({
+    label: 'rank',
+    message: isOrdinalNumber,
+  })
 
 t.create('daily rank (valid)')
   .get('/rd/rails.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rank',
-      value: isOrdinalNumberDaily,
-    })
-  )
+  .expectBadge({
+    label: 'rank',
+    message: isOrdinalNumberDaily,
+  })
 
 t.create('rank (not found)')
   .get('/rt/not-a-package.json')
-  .expectJSON({ name: 'rank', value: 'not found' })
+  .expectBadge({ label: 'rank', message: 'not found' })
 
 t.create('rank is null')
   .get('/rd/rails.json')
@@ -42,4 +38,4 @@ t.create('rank is null')
         },
       ])
   )
-  .expectJSON({ name: 'rank', value: 'invalid rank' })
+  .expectBadge({ label: 'rank', message: 'invalid rank' })
diff --git a/services/gem/gem-version.tester.js b/services/gem/gem-version.tester.js
index bef778ccb312d0c4e7de70d9e618268490a63b25..88caac14f49c45c4f9d9b1e3ff07389e60c79714 100644
--- a/services/gem/gem-version.tester.js
+++ b/services/gem/gem-version.tester.js
@@ -1,18 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('version (valid)')
   .get('/formatador.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'gem',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'gem',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('version (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'gem', value: 'not found' })
+  .expectBadge({ label: 'gem', message: 'not found' })
diff --git a/services/gemnasium/gemnasium.tester.js b/services/gemnasium/gemnasium.tester.js
index 62357635747f415e5cf87b36e1e305924187e8b6..152a1ed98577c5088ca3bb3c7be5dc0c6849c763 100644
--- a/services/gemnasium/gemnasium.tester.js
+++ b/services/gemnasium/gemnasium.tester.js
@@ -9,7 +9,7 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (previously dependencies)')
   .get('/mathiasbynens/he.json')
-  .expectJSON({
-    name: 'gemnasium',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'gemnasium',
+    message: 'no longer available',
   })
diff --git a/services/github/github-commit-activity.tester.js b/services/github/github-commit-activity.tester.js
index 24a0175f6453384f513fa03963743903985b7a48..701d3723e18ceeb65625262342e7af0523e42eae 100644
--- a/services/github/github-commit-activity.tester.js
+++ b/services/github/github-commit-activity.tester.js
@@ -1,48 +1,39 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetricOverTimePeriod } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('commit activity (1 year)')
   .get('/y/eslint/eslint.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'commit activity',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'commit activity',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('commit activity (1 month)')
   .get('/m/eslint/eslint.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'commit activity',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'commit activity',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('commit activity (4 weeks)')
   .get('/4w/eslint/eslint.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'commit activity',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'commit activity',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('commit activity (1 week)')
   .get('/w/eslint/eslint.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'commit activity',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'commit activity',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('commit activity (repo not found)')
   .get('/w/badges/helmets.json')
-  .expectJSON({
-    name: 'commit activity',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'commit activity',
+    message: 'repo not found',
   })
diff --git a/services/github/github-commit-status.tester.js b/services/github/github-commit-status.tester.js
index e6060dddac8cb267ec64b963abd60df0ddda78ef..757113fb553b2b608b3a46e14212504227dd45d3 100644
--- a/services/github/github-commit-status.tester.js
+++ b/services/github/github-commit-status.tester.js
@@ -7,9 +7,9 @@ t.create('commit status - commit in branch')
   .get(
     '/badges/shields/master/5d4ab86b1b5ddfb3c4a70a70bd19932c52603b8c.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'in master',
+  .expectBadge({
+    label: 'commit status',
+    message: 'in master',
     color: 'brightgreen',
   })
 
@@ -26,9 +26,9 @@ t.create(
       )
       .reply(200, { status: 'identical' })
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'in master',
+  .expectBadge({
+    label: 'commit status',
+    message: 'in master',
     color: 'brightgreen',
   })
 
@@ -36,9 +36,9 @@ t.create('commit status - commit not in branch')
   .get(
     '/badges/shields/master/960c5bf72d7d1539fcd453343eed3f8617427a41.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'commit or branch not found',
+  .expectBadge({
+    label: 'commit status',
+    message: 'commit or branch not found',
     color: 'lightgrey',
   })
 
@@ -46,9 +46,9 @@ t.create('commit status - unknown commit id')
   .get(
     '/atom/atom/v1.27.1/7dfb45eb61a48a4ce18a0dd2e31f944ed4467ae3.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'not in v1.27.1',
+  .expectBadge({
+    label: 'commit status',
+    message: 'not in v1.27.1',
     color: 'yellow',
   })
 
@@ -56,9 +56,9 @@ t.create('commit status - unknown branch')
   .get(
     '/badges/shields/this-branch-does-not-exist/b551a3a8daf1c48dba32a3eab1edf99b10c28863.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'commit or branch not found',
+  .expectBadge({
+    label: 'commit status',
+    message: 'commit or branch not found',
     color: 'lightgrey',
   })
 
@@ -66,9 +66,9 @@ t.create('commit status - no common ancestor between commit and branch')
   .get(
     '/badges/shields/master/b551a3a8daf1c48dba32a3eab1edf99b10c28863.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'no common ancestor',
+  .expectBadge({
+    label: 'commit status',
+    message: 'no common ancestor',
     color: 'lightgrey',
   })
 
@@ -83,9 +83,9 @@ t.create('commit status - invalid JSON')
       )
       .reply(invalidJSON)
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'invalid',
+  .expectBadge({
+    label: 'commit status',
+    message: 'invalid',
     color: 'lightgrey',
   })
 
@@ -94,9 +94,9 @@ t.create('commit status - network error')
     '/badges/shields/master/5d4ab86b1b5ddfb3c4a70a70bd19932c52603b8c.json?style=_shields_test'
   )
   .networkOff()
-  .expectJSON({
-    name: 'commit status',
-    value: 'inaccessible',
+  .expectBadge({
+    label: 'commit status',
+    message: 'inaccessible',
     color: 'red',
   })
 
@@ -111,9 +111,9 @@ t.create('commit status - github server error')
       )
       .reply(500)
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'invalid',
+  .expectBadge({
+    label: 'commit status',
+    message: 'invalid',
     color: 'lightgrey',
   })
 
@@ -128,9 +128,9 @@ t.create('commit status - 404 with empty JSON form github')
       )
       .reply(404, {})
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'invalid',
+  .expectBadge({
+    label: 'commit status',
+    message: 'invalid',
     color: 'lightgrey',
   })
 
@@ -145,8 +145,8 @@ t.create('commit status - 404 with invalid JSON form github')
       )
       .reply(404, invalidJSON)
   )
-  .expectJSON({
-    name: 'commit status',
-    value: 'invalid',
+  .expectBadge({
+    label: 'commit status',
+    message: 'invalid',
     color: 'lightgrey',
   })
diff --git a/services/github/github-commits-since.tester.js b/services/github/github-commits-since.tester.js
index 9473f8d67d1d85058165beb67572b6cdc0491e19..544cee53f5796c466758c90aa9d91368c0036f9d 100644
--- a/services/github/github-commits-since.tester.js
+++ b/services/github/github-commits-since.tester.js
@@ -7,19 +7,15 @@ t.create('Commits since')
   .get(
     '/badges/shields/a0663d8da53fb712472c02665e6ff7547ba945b7.json?style=_shields_test'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: Joi.string().regex(/^(commits since){1}[\s\S]+$/),
-      value: Joi.string().regex(/^\w+$/),
-      color: 'blue',
-    })
-  )
+  .expectBadge({
+    label: Joi.string().regex(/^(commits since){1}[\s\S]+$/),
+    message: Joi.string().regex(/^\w+$/),
+    color: 'blue',
+  })
 
 t.create('Commits since by latest release')
   .get('/microsoft/typescript/latest.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: Joi.string().regex(/^(commits since){1}[\s\S]+$/),
-      value: Joi.string().regex(/^\d+\w?$/),
-    })
-  )
+  .expectBadge({
+    label: Joi.string().regex(/^(commits since){1}[\s\S]+$/),
+    message: Joi.string().regex(/^\d+\w?$/),
+  })
diff --git a/services/github/github-contributors.tester.js b/services/github/github-contributors.tester.js
index f6977e9312bb305e6e45aab954e3efbf96e30947..49a669f3625938e2366340d6a4123ea7bb7c31da 100644
--- a/services/github/github-contributors.tester.js
+++ b/services/github/github-contributors.tester.js
@@ -1,28 +1,25 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { isMetric } = require('../test-validators')
 
 t.create('Contributors')
   .get('/contributors/cdnjs/cdnjs.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'contributors',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'contributors',
+    message: isMetric,
+  })
 
 t.create('1 contributor')
   .get('/contributors/paulmelnikow/local-credential-storage.json')
-  .expectJSON({
-    name: 'contributors',
-    value: '1',
+  .expectBadge({
+    label: 'contributors',
+    message: '1',
   })
 
 t.create('Contributors (repo not found)')
   .get('/contributors/badges/helmets.json')
-  .expectJSON({
-    name: 'contributors',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'contributors',
+    message: 'repo not found',
   })
diff --git a/services/github/github-downloads.tester.js b/services/github/github-downloads.tester.js
index c23d1065dde6c77af8a84ca60d25810ac20e1488..64ea5ca00b9955d84414ea7b1b7a6469df46e673 100644
--- a/services/github/github-downloads.tester.js
+++ b/services/github/github-downloads.tester.js
@@ -6,86 +6,72 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Downloads all releases')
   .get('/downloads/photonstorm/phaser/total.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(/^\w+\s+total$/),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(/^\w+\s+total$/),
+  })
 
 t.create('Downloads all releases (repo not found)')
   .get('/downloads/badges/helmets/total.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'repo or release not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'repo or release not found',
   })
 
 t.create('downloads for latest release')
   .get('/downloads/photonstorm/phaser/latest/total.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('downloads-pre for latest release')
   .get('/downloads-pre/photonstorm/phaser/latest/total.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('downloads for release without slash')
   .get('/downloads/atom/atom/v0.190.0/total.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? v0\.190\.0$/),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? v0\.190\.0$/),
+  })
 
 t.create('downloads for specific asset without slash')
   .get('/downloads/atom/atom/v0.190.0/atom-amd64.deb.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(
-        /^[0-9]+[kMGTPEZY]? v0\.190\.0 \[atom-amd64\.deb\]$/
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(
+      /^[0-9]+[kMGTPEZY]? v0\.190\.0 \[atom-amd64\.deb\]$/
+    ),
+  })
 
 t.create('downloads for specific asset from latest release')
   .get('/downloads/atom/atom/latest/atom-amd64.deb.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/),
+  })
 
 t.create('downloads-pre for specific asset from latest release')
   .get('/downloads-pre/atom/atom/latest/atom-amd64.deb.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? \[atom-amd64\.deb\]$/),
+  })
 
 t.create('downloads for release with slash')
   .get('/downloads/NHellFire/dban/stable/v2.2.8/total.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? stable\/v2\.2\.8$/),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? stable\/v2\.2\.8$/),
+  })
 
 t.create('downloads for specific asset with slash')
   .get('/downloads/NHellFire/dban/stable/v2.2.8/dban-2.2.8_i586.iso.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: Joi.string().regex(
-        /^[0-9]+[kMGTPEZY]? stable\/v2\.2\.8 \[dban-2\.2\.8_i586\.iso\]$/
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: Joi.string().regex(
+      /^[0-9]+[kMGTPEZY]? stable\/v2\.2\.8 \[dban-2\.2\.8_i586\.iso\]$/
+    ),
+  })
 
 t.create('downloads for unknown release')
   .get('/downloads/atom/atom/does-not-exist/total.json')
-  .expectJSON({ name: 'downloads', value: 'repo or release not found' })
+  .expectBadge({ label: 'downloads', message: 'repo or release not found' })
diff --git a/services/github/github-followers.tester.js b/services/github/github-followers.tester.js
index e4db41571bb2fedea9a60b0feff4a30f8d5038e9..8e1fd07505c67eae7918510eaefcdeeaf0327cbf 100644
--- a/services/github/github-followers.tester.js
+++ b/services/github/github-followers.tester.js
@@ -5,16 +5,14 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Followers')
   .get('/webcaetano.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'followers',
-      value: Joi.string().regex(/^\w+$/),
-    })
-  )
+  .expectBadge({
+    label: 'followers',
+    message: Joi.string().regex(/^\w+$/),
+  })
 
 t.create('Followers (user not found)')
   .get('/PyvesB2.json')
-  .expectJSON({
-    name: 'followers',
-    value: 'user not found',
+  .expectBadge({
+    label: 'followers',
+    message: 'user not found',
   })
diff --git a/services/github/github-forks.tester.js b/services/github/github-forks.tester.js
index f54079d7fb1c0494aaacc16a3f17d76cfc070c80..2aca3dad239b0f61a3cf97be69563469d8a96b6b 100644
--- a/services/github/github-forks.tester.js
+++ b/services/github/github-forks.tester.js
@@ -5,18 +5,16 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Forks')
   .get('/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'forks',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'forks',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('Forks (repo not found)')
   .get('/badges/helmets.json')
-  .expectJSON({
-    name: 'forks',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'forks',
+    message: 'repo not found',
   })
diff --git a/services/github/github-issue-detail.tester.js b/services/github/github-issue-detail.tester.js
index 2ce42efc0eea8a70edafe801a0b707fa2fbd910e..82d2c2130345918ee7a0241d1ffab16efd2628cd 100644
--- a/services/github/github-issue-detail.tester.js
+++ b/services/github/github-issue-detail.tester.js
@@ -6,62 +6,50 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('github issue state')
   .get('/s/badges/shields/979.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issue 979',
-      value: Joi.equal('open', 'closed'),
-    })
-  )
+  .expectBadge({
+    label: 'issue 979',
+    message: Joi.equal('open', 'closed'),
+  })
 
 t.create('github issue state (repo not found)')
   .get('/s/badges/helmets/979.json')
-  .expectJSON({
-    name: 'issue/pull request 979',
-    value: 'issue, pull request or repo not found',
+  .expectBadge({
+    label: 'issue/pull request 979',
+    message: 'issue, pull request or repo not found',
   })
 
 t.create('github issue title')
   .get('/title/badges/shields/979.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issue 979',
-      value: 'Github rate limits cause transient service test failures in CI',
-    })
-  )
+  .expectBadge({
+    label: 'issue 979',
+    message: 'Github rate limits cause transient service test failures in CI',
+  })
 
 t.create('github issue author')
   .get('/u/badges/shields/979.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'author', value: 'paulmelnikow' }))
+  .expectBadge({ label: 'author', message: 'paulmelnikow' })
 
 t.create('github issue label')
   .get('/label/badges/shields/979.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'label',
-      value: Joi.equal(
-        'bug | developer-experience',
-        'developer-experience | bug'
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'label',
+    message: Joi.equal(
+      'bug | developer-experience',
+      'developer-experience | bug'
+    ),
+  })
 
 t.create('github issue comments')
   .get('/comments/badges/shields/979.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'comments',
-      value: Joi.number().greater(15),
-    })
-  )
+  .expectBadge({
+    label: 'comments',
+    message: Joi.number().greater(15),
+  })
 
 t.create('github issue age')
   .get('/age/badges/shields/979.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'created', value: isFormattedDate })
-  )
+  .expectBadge({ label: 'created', message: isFormattedDate })
 
 t.create('github issue update')
   .get('/last-update/badges/shields/979.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'updated', value: isFormattedDate })
-  )
+  .expectBadge({ label: 'updated', message: isFormattedDate })
diff --git a/services/github/github-issues.tester.js b/services/github/github-issues.tester.js
index aafe57afe0a77effcc5759cf82bcdacc9cd74715..220ecf33009c6f3389d0fd449c39221fe59d6ee5 100644
--- a/services/github/github-issues.tester.js
+++ b/services/github/github-issues.tester.js
@@ -6,130 +6,104 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('GitHub closed pull requests')
   .get('/issues-pr-closed/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? closed$/),
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? closed$/),
+  })
 
 t.create('GitHub closed pull requests raw')
   .get('/issues-pr-closed-raw/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'closed pull requests',
-      value: Joi.string().regex(/^\w+?$/),
-    })
-  )
+  .expectBadge({
+    label: 'closed pull requests',
+    message: Joi.string().regex(/^\w+?$/),
+  })
 
 t.create('GitHub pull requests')
   .get('/issues-pr/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pull requests',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'pull requests',
+    message: isMetricOpenIssues,
+  })
 
 t.create('GitHub pull requests raw')
   .get('/issues-pr-raw/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'open pull requests',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'open pull requests',
+    message: isMetric,
+  })
 
 t.create('GitHub closed issues')
   .get('/issues-closed/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issues',
-      value: Joi.string().regex(/^[0-9]+[kMGTPEZY]? closed$/),
-    })
-  )
+  .expectBadge({
+    label: 'issues',
+    message: Joi.string().regex(/^[0-9]+[kMGTPEZY]? closed$/),
+  })
 
 t.create('GitHub closed issues raw')
   .get('/issues-closed-raw/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'closed issues',
-      value: Joi.string().regex(/^\w+\+?$/),
-    })
-  )
+  .expectBadge({
+    label: 'closed issues',
+    message: Joi.string().regex(/^\w+\+?$/),
+  })
 
 t.create('GitHub open issues')
   .get('/issues/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'issues',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'issues',
+    message: isMetricOpenIssues,
+  })
 
 t.create('GitHub open issues raw')
   .get('/issues-raw/badges/shields.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'open issues', value: isMetric }))
+  .expectBadge({ label: 'open issues', message: isMetric })
 
 t.create('GitHub open issues by label is > zero')
   .get('/issues/badges/shields/service-badge.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'service-badge issues',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'service-badge issues',
+    message: isMetricOpenIssues,
+  })
 
 t.create('GitHub open issues by multi-word label is > zero')
   .get('/issues/Cockatrice/Cockatrice/App%20-%20Cockatrice.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: '"app - cockatrice" issues',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: '"app - cockatrice" issues',
+    message: isMetricOpenIssues,
+  })
 
 t.create('GitHub open issues by label (raw)')
   .get('/issues-raw/badges/shields/service-badge.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'open service-badge issues',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'open service-badge issues',
+    message: isMetric,
+  })
 
 // https://github.com/badges/shields/issues/1870
 t.create('GitHub open issues by label including slash character (raw)')
   .get('/issues-raw/IgorNovozhilov/ndk/@ndk/cfg.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'open @ndk/cfg issues',
-      // Not always > 0.
-      value: Joi.alternatives(isMetric, Joi.equal('0')),
-    })
-  )
+  .expectBadge({
+    label: 'open @ndk/cfg issues',
+    // Not always > 0.
+    message: Joi.alternatives(isMetric, Joi.equal('0')),
+  })
 
 t.create('GitHub open issues (repo not found)')
   .get('/issues-raw/badges/helmets.json')
-  .expectJSON({
-    name: 'open issues',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'open issues',
+    message: 'repo not found',
   })
 
 t.create('GitHub open pull requests by label')
   .get('/issues-pr/badges/shields/service-badge.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'service-badge pull requests',
-      value: isMetricOpenIssues,
-    })
-  )
+  .expectBadge({
+    label: 'service-badge pull requests',
+    message: isMetricOpenIssues,
+  })
 
 t.create('GitHub open pull requests by label (raw)')
   .get('/issues-pr-raw/badges/shields/service-badge.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'open service-badge pull requests',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'open service-badge pull requests',
+    message: isMetric,
+  })
diff --git a/services/github/github-languages.tester.js b/services/github/github-languages.tester.js
index 9dbc142ed0e414859d036ff68978ed06a69a1f0c..c6b5100c63ab391ac286ace909bea6900ab464d4 100644
--- a/services/github/github-languages.tester.js
+++ b/services/github/github-languages.tester.js
@@ -12,50 +12,42 @@ const t = (module.exports = new ServiceTester({
 
 t.create('top language')
   .get('/top/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'javascript',
-      value: Joi.string().regex(/^([1-9]?[0-9]\.[0-9]|100\.0)%$/),
-    })
-  )
+  .expectBadge({
+    label: 'javascript',
+    message: Joi.string().regex(/^([1-9]?[0-9]\.[0-9]|100\.0)%$/),
+  })
 
 t.create('top language')
   .get('/top/badges/shields.json?colorB=123&format=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'javascript',
-      value: Joi.string().regex(/^([1-9]?[0-9]\.[0-9]|100\.0)%$/),
-      color: '#123',
-    })
-  )
+  .expectBadge({
+    label: 'javascript',
+    message: Joi.string().regex(/^([1-9]?[0-9]\.[0-9]|100\.0)%$/),
+    color: '#123',
+  })
 
 t.create('top language with empty repository')
   .get('/top/pyvesb/emptyrepo.json')
-  .expectJSON({ name: 'language', value: 'none' })
+  .expectBadge({ label: 'language', message: 'none' })
 
 t.create('language count')
   .get('/count/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'languages',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'languages',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('language count (repo not found)')
   .get('/count/badges/helmets.json')
-  .expectJSON({
-    name: 'languages',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'languages',
+    message: 'repo not found',
   })
 
 t.create('code size in bytes for all languages')
   .get('/code-size/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'code size',
-      value: isFileSize,
-    })
-  )
+  .expectBadge({
+    label: 'code size',
+    message: isFileSize,
+  })
diff --git a/services/github/github-last-commit.tester.js b/services/github/github-last-commit.tester.js
index 1af1f53307c576f395f521047bf6ae32665f1730..5ee66e832de3ecf9dd6c4d92270b723ff9b1ae91 100644
--- a/services/github/github-last-commit.tester.js
+++ b/services/github/github-last-commit.tester.js
@@ -1,23 +1,20 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFormattedDate } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('last commit (recent)')
   .get('/eslint/eslint.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'last commit', value: isFormattedDate })
-  )
+  .expectBadge({ label: 'last commit', message: isFormattedDate })
 
 t.create('last commit (ancient)')
   .get('/badges/badgr.co.json')
-  .expectJSON({ name: 'last commit', value: 'january 2014' })
+  .expectBadge({ label: 'last commit', message: 'january 2014' })
 
 t.create('last commit (on branch)')
   .get('/badges/badgr.co/shielded.json')
-  .expectJSON({ name: 'last commit', value: 'july 2013' })
+  .expectBadge({ label: 'last commit', message: 'july 2013' })
 
 t.create('last commit (repo not found)')
   .get('/badges/helmets.json')
-  .expectJSON({ name: 'last commit', value: 'repo not found' })
+  .expectBadge({ label: 'last commit', message: 'repo not found' })
diff --git a/services/github/github-license.tester.js b/services/github/github-license.tester.js
index b3cf40d59a653340151b1187eac49e039e199bfb..fc582255e0215b335efc29124c4ddb6dcb28881a 100644
--- a/services/github/github-license.tester.js
+++ b/services/github/github-license.tester.js
@@ -8,21 +8,25 @@ const unknownLicenseColor = licenseToColor()
 
 t.create('License')
   .get('/github/gitignore.json?style=_shields_test')
-  .expectJSON({
-    name: 'license',
-    value: 'CC0-1.0',
+  .expectBadge({
+    label: 'license',
+    message: 'CC0-1.0',
     color: `#${publicDomainLicenseColor}`,
   })
 
 t.create('License for repo without a license')
   .get('/badges/badger.json?style=_shields_test')
-  .expectJSON({ name: 'license', value: 'not specified', color: 'lightgrey' })
+  .expectBadge({
+    label: 'license',
+    message: 'not specified',
+    color: 'lightgrey',
+  })
 
 t.create('License for repo with an unrecognized license')
   .get('/philokev/sopel-noblerealms.json?style=_shields_test')
-  .expectJSON({
-    name: 'license',
-    value: 'not identifiable by github',
+  .expectBadge({
+    label: 'license',
+    message: 'not identifiable by github',
     color: unknownLicenseColor,
   })
 
@@ -43,16 +47,16 @@ t.create('License with SPDX id not appearing in configuration')
         },
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'EFL-1.0',
+  .expectBadge({
+    label: 'license',
+    message: 'EFL-1.0',
     color: unknownLicenseColor,
   })
 
 t.create('License for unknown repo')
   .get('/user1/github-does-not-have-this-repo.json?style=_shields_test')
-  .expectJSON({
-    name: 'license',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'license',
+    message: 'repo not found',
     color: 'red',
   })
diff --git a/services/github/github-manifest.tester.js b/services/github/github-manifest.tester.js
index b7a4253bdcba877784ef930c8f7497701951ed2b..0223d6dff006dc2197b243c480e500def08a0ee9 100644
--- a/services/github/github-manifest.tester.js
+++ b/services/github/github-manifest.tester.js
@@ -12,33 +12,29 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Manifest version')
   .get('/v/RedSparr0w/IndieGala-Helper.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'version',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'version',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Manifest name')
   .get('/n/RedSparr0w/IndieGala-Helper.json')
-  .expectJSON({ name: 'name', value: 'IndieGala Helper' })
+  .expectBadge({ label: 'name', message: 'IndieGala Helper' })
 
 t.create('Manifest array')
   .get('/permissions/RedSparr0w/IndieGala-Helper.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'permissions',
-      value: Joi.string().regex(/.*?,/),
-    })
-  )
+  .expectBadge({
+    label: 'permissions',
+    message: Joi.string().regex(/.*?,/),
+  })
 
 t.create('Manifest object')
   .get('/background/RedSparr0w/IndieGala-Helper.json')
-  .expectJSON({ name: 'manifest', value: 'invalid key value' })
+  .expectBadge({ label: 'manifest', message: 'invalid key value' })
 
 t.create('Manifest invalid json response')
   .get('/v/RedSparr0w/not-a-real-project.json')
-  .expectJSON({
-    name: 'version',
-    value: 'repo not found, branch not found, or manifest.json missing',
+  .expectBadge({
+    label: 'version',
+    message: 'repo not found, branch not found, or manifest.json missing',
   })
diff --git a/services/github/github-package-json.tester.js b/services/github/github-package-json.tester.js
index f187eee7ea9eae88ba122c339e369969f74f4f30..0fd455fe1f4c967781f50e5f1db83ec6b39c114d 100644
--- a/services/github/github-package-json.tester.js
+++ b/services/github/github-package-json.tester.js
@@ -13,91 +13,77 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Package version')
   .get('/v/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'version',
-      value: isSemver,
-    })
-  )
+  .expectBadge({
+    label: 'version',
+    message: isSemver,
+  })
 
 t.create('Package version (repo not found)')
   .get('/v/badges/helmets.json')
-  .expectJSON({
-    name: 'version',
-    value: 'repo not found, branch not found, or package.json missing',
+  .expectBadge({
+    label: 'version',
+    message: 'repo not found, branch not found, or package.json missing',
   })
 
 t.create('Package name')
   .get('/n/badges/shields.json')
-  .expectJSON({ name: 'name', value: 'shields.io' })
+  .expectBadge({ label: 'name', message: 'shields.io' })
 
 t.create('Package name - Custom label')
   .get('/name/badges/shields.json?label=Dev Name')
-  .expectJSON({ name: 'Dev Name', value: 'shields.io' })
+  .expectBadge({ label: 'Dev Name', message: 'shields.io' })
 
 t.create('Package array')
   .get('/keywords/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'keywords',
-      value: Joi.string().regex(/.*?,/),
-    })
-  )
+  .expectBadge({
+    label: 'keywords',
+    message: Joi.string().regex(/.*?,/),
+  })
 
 t.create('Package object')
   .get('/dependencies/badges/shields.json')
-  .expectJSON({ name: 'package.json', value: 'invalid key value' })
+  .expectBadge({ label: 'package.json', message: 'invalid key value' })
 
 t.create('Peer dependency version')
   .get('/dependency-version/paulmelnikow/react-boxplot/peer/react.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'react',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'react',
+    message: semverRange,
+  })
 
 t.create('Dev dependency version')
   .get(
     '/dependency-version/paulmelnikow/react-boxplot/dev/react.json?label=react%20tested'
   )
-  .expectJSONTypes(
-    Joi.object({
-      name: 'react tested',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'react tested',
+    message: semverRange,
+  })
 
 t.create('Prod prod dependency version')
   .get('/dependency-version/paulmelnikow/react-boxplot/simple-statistics.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'simple-statistics',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'simple-statistics',
+    message: semverRange,
+  })
 
 t.create('Scoped dependency')
   .get('/dependency-version/badges/shields/dev/@babel/core.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: '@babel/core',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: '@babel/core',
+    message: semverRange,
+  })
 
 t.create('Scoped dependency on branch')
   .get('/dependency-version/zeit/next.js/dev/babel-eslint/alpha.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'babel-eslint',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'babel-eslint',
+    message: semverRange,
+  })
 
 t.create('Unknown dependency')
   .get('/dependency-version/paulmelnikow/react-boxplot/dev/i-made-this-up.json')
-  .expectJSON({
-    name: 'dependency',
-    value: 'dev dependency not found',
+  .expectBadge({
+    label: 'dependency',
+    message: 'dev dependency not found',
   })
diff --git a/services/github/github-pull-request-check-state.tester.js b/services/github/github-pull-request-check-state.tester.js
index a7df0b57669fda0ed3eb47b03bc5290bc0cfd1c4..1505846992b80faf92b1a0591537c6898c286ffb 100644
--- a/services/github/github-pull-request-check-state.tester.js
+++ b/services/github/github-pull-request-check-state.tester.js
@@ -4,11 +4,11 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('github pull request check state')
   .get('/s/pulls/badges/shields/1110.json')
-  .expectJSON({ name: 'checks', value: 'failure' })
+  .expectBadge({ label: 'checks', message: 'failure' })
 
 t.create('github pull request check state (pull request not found)')
   .get('/s/pulls/badges/shields/5110.json')
-  .expectJSON({ name: 'checks', value: 'pull request or repo not found' })
+  .expectBadge({ label: 'checks', message: 'pull request or repo not found' })
 
 t.create(
   "github pull request check state (ref returned by github doesn't exist"
@@ -21,11 +21,11 @@ t.create(
         .reply(200, JSON.stringify({ head: { sha: 'abc123' } })) // Looks like a real ref, but isn't.
   )
   .networkOn()
-  .expectJSON({
-    name: 'checks',
-    value: 'commit not found',
+  .expectBadge({
+    label: 'checks',
+    message: 'commit not found',
   })
 
 t.create('github pull request check contexts')
   .get('/contexts/pulls/badges/shields/1110.json')
-  .expectJSON({ name: 'checks', value: '1 failure' })
+  .expectBadge({ label: 'checks', message: '1 failure' })
diff --git a/services/github/github-release-date.tester.js b/services/github/github-release-date.tester.js
index 6e0fd0f04bdec0442b5929306fcb2426a45b7f6a..024bc6d900d33f089cb1b98c9781626aea9e42e8 100644
--- a/services/github/github-release-date.tester.js
+++ b/services/github/github-release-date.tester.js
@@ -1,53 +1,50 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFormattedDate } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Release Date. e.g release date|today')
   .get('/release-date/microsoft/vscode.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'release date',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'release date',
+    message: isFormattedDate,
+  })
 
 t.create('Release Date - Custom Label. e.g myRelease|today')
   .get('/release-date/microsoft/vscode.json?label=myRelease')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'myRelease',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'myRelease',
+    message: isFormattedDate,
+  })
 
 t.create(
   'Release Date - Should return `no releases or repo not found` for invalid repo'
 )
   .get('/release-date/not-valid-name/not-valid-repo.json')
-  .expectJSON({ name: 'release date', value: 'no releases or repo not found' })
+  .expectBadge({
+    label: 'release date',
+    message: 'no releases or repo not found',
+  })
 
 t.create('(Pre-)Release Date. e.g release date|today')
   .get('/release-date-pre/microsoft/vscode.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'release date',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'release date',
+    message: isFormattedDate,
+  })
 
 t.create('(Pre-)Release Date - Custom Label. e.g myRelease|today')
   .get('/release-date-pre/microsoft/vscode.json?label=myRelease')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'myRelease',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'myRelease',
+    message: isFormattedDate,
+  })
 
 t.create(
   '(Pre-)Release Date - Should return `no releases or repo not found` for invalid repo'
 )
   .get('/release-date-pre/not-valid-name/not-valid-repo.json')
-  .expectJSON({ name: 'release date', value: 'no releases or repo not found' })
+  .expectBadge({
+    label: 'release date',
+    message: 'no releases or repo not found',
+  })
diff --git a/services/github/github-release.tester.js b/services/github/github-release.tester.js
index 67933044411feb44950fce72fc286c8ceb5696f0..d813a97d2c17d8879fdfa555088a558cd108a776 100644
--- a/services/github/github-release.tester.js
+++ b/services/github/github-release.tester.js
@@ -1,32 +1,27 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusTripleDottedVersion } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Release')
   .get('/release/photonstorm/phaser.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'release', value: isVPlusTripleDottedVersion })
-  )
+  .expectBadge({ label: 'release', message: isVPlusTripleDottedVersion })
 
 t.create('Release (repo not found)')
   .get('/release/badges/helmets.json')
-  .expectJSON({ name: 'release', value: 'no releases or repo not found' })
+  .expectBadge({ label: 'release', message: 'no releases or repo not found' })
 
 t.create('Prerelease')
   .get('/release-pre/photonstorm/phaser.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'release', value: isVPlusTripleDottedVersion })
-  )
+  .expectBadge({ label: 'release', message: isVPlusTripleDottedVersion })
 
 t.create('Prerelease (repo not found)')
   .get('/release-pre/badges/helmets.json')
-  .expectJSON({ name: 'release', value: 'no releases or repo not found' })
+  .expectBadge({ label: 'release', message: 'no releases or repo not found' })
 
 t.create('No releases')
   .get('/release/badges/daily-tests.json')
-  .expectJSON({
-    name: 'release',
-    value: 'no releases or repo not found',
+  .expectBadge({
+    label: 'release',
+    message: 'no releases or repo not found',
   })
diff --git a/services/github/github-repo-size.tester.js b/services/github/github-repo-size.tester.js
index fde82ee9eb0904ba6b19478cc02bc33e137af3f6..cc55e28378dbd74e548ec691f8ac5d2df712a151 100644
--- a/services/github/github-repo-size.tester.js
+++ b/services/github/github-repo-size.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFileSize } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('repository size')
   .get('/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'repo size',
-      value: isFileSize,
-    })
-  )
+  .expectBadge({
+    label: 'repo size',
+    message: isFileSize,
+  })
 
 t.create('repository size (repo not found)')
   .get('/badges/helmets.json')
-  .expectJSON({
-    name: 'repo size',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'repo size',
+    message: 'repo not found',
   })
diff --git a/services/github/github-search.tester.js b/services/github/github-search.tester.js
index ba450b0f30a8aba227da3f50adfd5c8608880678..83dd403194404c39f4392da0d9d330697bb64a68 100644
--- a/services/github/github-search.tester.js
+++ b/services/github/github-search.tester.js
@@ -1,15 +1,12 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('hit counter')
   .get('/badges/shields/async%20handle.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'async handle counter', value: isMetric })
-  )
+  .expectBadge({ label: 'async handle counter', message: isMetric })
 
 t.create('hit counter for nonexistent repo')
   .get('/badges/puppets/async%20handle.json')
-  .expectJSON({ name: 'counter', value: 'repo not found' })
+  .expectBadge({ label: 'counter', message: 'repo not found' })
diff --git a/services/github/github-size.tester.js b/services/github/github-size.tester.js
index 29cdee081273895eab99affc765ec9fe53622e6c..9645a469eee22ebdd021b1a47a4b5d7d3cbe1b6a 100644
--- a/services/github/github-size.tester.js
+++ b/services/github/github-size.tester.js
@@ -1,17 +1,16 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFileSize } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('File size')
   .get('/webcaetano/craft/build/phaser-craft.min.js.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'size', value: isFileSize }))
+  .expectBadge({ label: 'size', message: isFileSize })
 
 t.create('File size 404')
   .get('/webcaetano/craft/build/does-not-exist.min.js.json')
-  .expectJSON({ name: 'size', value: 'repo or file not found' })
+  .expectBadge({ label: 'size', message: 'repo or file not found' })
 
 t.create('File size for "not a regular file"')
   .get('/webcaetano/craft/build.json')
-  .expectJSON({ name: 'size', value: 'not a regular file' })
+  .expectBadge({ label: 'size', message: 'not a regular file' })
diff --git a/services/github/github-stars.tester.js b/services/github/github-stars.tester.js
index 78983c2b2638de753b1656615018c1c4291c6cae..b6c4505fd9de57880ddb4e1bbddb15e27eef97e2 100644
--- a/services/github/github-stars.tester.js
+++ b/services/github/github-stars.tester.js
@@ -5,36 +5,30 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Stars')
   .get('/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: Joi.string().regex(/^\w+$/),
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: Joi.string().regex(/^\w+$/),
+  })
 
 t.create('Stars (repo not found)')
   .get('/badges/helmets.json')
-  .expectJSON({
-    name: 'stars',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'stars',
+    message: 'repo not found',
   })
 
 t.create('Stars (named color override)')
   .get('/badges/shields.json?color=yellow&style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: Joi.string().regex(/^\w+$/),
-      color: 'yellow',
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: Joi.string().regex(/^\w+$/),
+    color: 'yellow',
+  })
 
 t.create('Stars (hex color override)')
   .get('/badges/shields.json?color=abcdef&style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: Joi.string().regex(/^\w+$/),
-      color: '#abcdef',
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: Joi.string().regex(/^\w+$/),
+    color: '#abcdef',
+  })
diff --git a/services/github/github-tag.tester.js b/services/github/github-tag.tester.js
index da587dcd384a406d0e2191404ce6163b612962dc..bf5fbc7bb01907da05f43236199aaf43a1b5152d 100644
--- a/services/github/github-tag.tester.js
+++ b/services/github/github-tag.tester.js
@@ -5,15 +5,15 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Tag')
   .get('/tag/photonstorm/phaser.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'tag', value: Joi.string() }))
+  .expectBadge({ label: 'tag', message: Joi.string() })
 
 t.create('Tag (inc pre-release)')
   .get('/tag-pre/photonstorm/phaser.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'tag', value: Joi.string() }))
+  .expectBadge({ label: 'tag', message: Joi.string() })
 
 t.create('Tag (repo not found)')
   .get('/tag/badges/helmets.json')
-  .expectJSON({ name: 'tag', value: 'repo not found' })
+  .expectBadge({ label: 'tag', message: 'repo not found' })
 
 const tagsFixture = [
   { name: 'cheese' }, // any old string
@@ -28,7 +28,7 @@ t.create('Tag (mocked response, no pre-releases, semver ordering)')
       .get('/repos/foo/bar/tags')
       .reply(200, tagsFixture)
   )
-  .expectJSON({ name: 'tag', value: 'v1.2', color: 'blue' })
+  .expectBadge({ label: 'tag', message: 'v1.2', color: 'blue' })
 
 t.create('Tag (mocked response, include pre-releases, semver ordering)')
   .get('/tag-pre/foo/bar.json?style=_shields_test')
@@ -37,7 +37,7 @@ t.create('Tag (mocked response, include pre-releases, semver ordering)')
       .get('/repos/foo/bar/tags')
       .reply(200, tagsFixture)
   )
-  .expectJSON({ name: 'tag', value: 'v1.3-beta3', color: 'orange' })
+  .expectBadge({ label: 'tag', message: 'v1.3-beta3', color: 'orange' })
 
 t.create('Tag (mocked response, date ordering)')
   .get('/tag-date/foo/bar.json?style=_shields_test')
@@ -46,4 +46,4 @@ t.create('Tag (mocked response, date ordering)')
       .get('/repos/foo/bar/tags')
       .reply(200, tagsFixture)
   )
-  .expectJSON({ name: 'tag', value: 'cheese', color: 'blue' })
+  .expectBadge({ label: 'tag', message: 'cheese', color: 'blue' })
diff --git a/services/github/github-watchers.tester.js b/services/github/github-watchers.tester.js
index 60a279c92d395ab7b8f4ca0760448f1f49c86c6a..306e319d81512a814f3b2586aba93139d5aeba54 100644
--- a/services/github/github-watchers.tester.js
+++ b/services/github/github-watchers.tester.js
@@ -5,18 +5,16 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Watchers')
   .get('/badges/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'watchers',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'watchers',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('Watchers (repo not found)')
   .get('/badges/helmets.json')
-  .expectJSON({
-    name: 'watchers',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'watchers',
+    message: 'repo not found',
   })
diff --git a/services/gitlab/gitlab-pipeline-status.tester.js b/services/gitlab/gitlab-pipeline-status.tester.js
index 9f93440095d54a28f168b710074f385d7a1c0f63..0d015d2d9286f5cad62c02f34d975d9e91b1eb2b 100644
--- a/services/gitlab/gitlab-pipeline-status.tester.js
+++ b/services/gitlab/gitlab-pipeline-status.tester.js
@@ -1,46 +1,39 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Pipeline status')
   .get('/gitlab-org/gitlab-ce.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('Pipeline status (branch)')
   .get('/gitlab-org/gitlab-ce/v10.7.6.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('Pipeline status (nonexistent branch)')
   .get('/gitlab-org/gitlab-ce/nope-not-a-branch.json')
-  .expectJSON({
-    name: 'build',
-    value: 'branch not found',
+  .expectBadge({
+    label: 'build',
+    message: 'branch not found',
   })
 
 t.create('Pipeline status (nonexistent repo)')
   .get('/this-repo/does-not-exist.json')
-  .expectJSON({
-    name: 'build',
-    value: 'repo not found',
+  .expectBadge({
+    label: 'build',
+    message: 'repo not found',
   })
 
 t.create('Pipeline status (custom gitlab URL)')
   .get('/GNOME/pango.json?gitlab_url=https://gitlab.gnome.org')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
diff --git a/services/gitter/gitter.tester.js b/services/gitter/gitter.tester.js
index f8671b8fc77276f4a29c5ccffecd143a008955f2..84351df21234f300cfc5a1af2c12096ceafa7326 100644
--- a/services/gitter/gitter.tester.js
+++ b/services/gitter/gitter.tester.js
@@ -4,7 +4,7 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('on gitter')
   .get('/nwjs/nw.js.json')
-  .expectJSON({
-    name: 'chat',
-    value: 'on gitter',
+  .expectBadge({
+    label: 'chat',
+    message: 'on gitter',
   })
diff --git a/services/gratipay/gratipay.tester.js b/services/gratipay/gratipay.tester.js
index 6d3accfc1c8c853083913da2f08aa78f446dc72e..522ef69011de63718aa81ccfb71f4ef410756178 100644
--- a/services/gratipay/gratipay.tester.js
+++ b/services/gratipay/gratipay.tester.js
@@ -9,7 +9,7 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Receiving')
   .get('/Gratipay.json')
-  .expectJSON({
-    name: 'gratipay',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'gratipay',
+    message: 'no longer available',
   })
diff --git a/services/hackage/hackage-deps.tester.js b/services/hackage/hackage-deps.tester.js
index 432b8f9e0018f1b601d74edc200e952778a1ff28..caab395b12f29b8234cf4cb1d0dc17400ef996d1 100644
--- a/services/hackage/hackage-deps.tester.js
+++ b/services/hackage/hackage-deps.tester.js
@@ -5,13 +5,11 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('hackage deps (valid)')
   .get('/lens.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: Joi.string().regex(/^(up to date|outdated)$/),
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: Joi.string().regex(/^(up to date|outdated)$/),
+  })
 
 t.create('hackage deps (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'dependencies', value: 'not found' })
+  .expectBadge({ label: 'dependencies', message: 'not found' })
diff --git a/services/hackage/hackage-version.tester.js b/services/hackage/hackage-version.tester.js
index 770dfe46fec51ffeddf6a6ec0d2bae92731b7e88..b1092ef402f20e9eef1d642040810c40b4266c4e 100644
--- a/services/hackage/hackage-version.tester.js
+++ b/services/hackage/hackage-version.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('hackage version (valid)')
   .get('/lens.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'hackage',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'hackage',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('hackage version (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'hackage', value: 'not found' })
+  .expectBadge({ label: 'hackage', message: 'not found' })
 
 t.create('hackage version (unexpected response)')
   .get('/lens.json')
@@ -24,4 +21,4 @@ t.create('hackage version (unexpected response)')
       .get('/package/lens/lens.cabal')
       .reply(200, '')
   )
-  .expectJSON({ name: 'hackage', value: 'invalid response data' })
+  .expectBadge({ label: 'hackage', message: 'invalid response data' })
diff --git a/services/hexpm/hexpm.tester.js b/services/hexpm/hexpm.tester.js
index e9ad5734ea779184800e6b9662d76c65cb69f66d..0f86adc93389e4d785531c82b1a8bf3c873189e2 100644
--- a/services/hexpm/hexpm.tester.js
+++ b/services/hexpm/hexpm.tester.js
@@ -10,15 +10,11 @@ const t = (module.exports = new ServiceTester({ id: 'hexpm', title: 'Hex.pm' }))
 
 t.create('downloads per week')
   .get('/dw/cowboy.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'downloads', value: isMetricOverTimePeriod })
-  )
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('downloads per day')
   .get('/dd/cowboy.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'downloads', value: isMetricOverTimePeriod })
-  )
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('downloads (zero for period)')
   .get('/dd/cowboy.json')
@@ -31,33 +27,31 @@ t.create('downloads (zero for period)')
         meta: { licenses: ['MIT'] },
       })
   )
-  .expectJSON({ name: 'downloads', value: '0/day' })
+  .expectBadge({ label: 'downloads', message: '0/day' })
 
 t.create('downloads in total')
   .get('/dt/cowboy.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('downloads (not found)')
   .get('/dt/this-package-does-not-exist.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 t.create('version')
   .get('/v/cowboy.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'hex', value: isHexpmVersion }))
+  .expectBadge({ label: 'hex', message: isHexpmVersion })
 
 t.create('version (not found)')
   .get('/v/this-package-does-not-exist.json')
-  .expectJSON({ name: 'hex', value: 'not found' })
+  .expectBadge({ label: 'hex', message: 'not found' })
 
 t.create('license')
   .get('/l/cowboy.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'license',
-      value: Joi.string().required(),
-      color: 'blue',
-    })
-  )
+  .expectBadge({
+    label: 'license',
+    message: Joi.string().required(),
+    color: 'blue',
+  })
 
 t.create('license (multiple licenses)')
   .get('/l/cowboy.json?style=_shields_test')
@@ -70,9 +64,9 @@ t.create('license (multiple licenses)')
         meta: { licenses: ['GPLv2', 'MIT'] },
       })
   )
-  .expectJSON({
-    name: 'licenses',
-    value: 'GPLv2, MIT',
+  .expectBadge({
+    label: 'licenses',
+    message: 'GPLv2, MIT',
     color: 'blue',
   })
 
@@ -87,12 +81,12 @@ t.create('license (no license)')
         meta: { licenses: [] },
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'Unknown',
+  .expectBadge({
+    label: 'license',
+    message: 'Unknown',
     color: 'lightgrey',
   })
 
 t.create('license (not found)')
   .get('/l/this-package-does-not-exist.json')
-  .expectJSON({ name: 'license', value: 'not found' })
+  .expectBadge({ label: 'license', message: 'not found' })
diff --git a/services/homebrew/homebrew.tester.js b/services/homebrew/homebrew.tester.js
index 11ae25d6bde6b5a878a8b79c8349afb629bcd39b..9b067839d800c0d5f8a7adf633fe04168ec5ba79 100644
--- a/services/homebrew/homebrew.tester.js
+++ b/services/homebrew/homebrew.tester.js
@@ -1,17 +1,14 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusTripleDottedVersion } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('homebrew (valid)')
   .get('/cake.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'homebrew',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'homebrew',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('homebrew (valid, mocked response)')
   .get('/cake.json')
@@ -20,8 +17,8 @@ t.create('homebrew (valid, mocked response)')
       .get('/api/formula/cake.json')
       .reply(200, { versions: { stable: '0.23.0', devel: null, head: null } })
   )
-  .expectJSON({ name: 'homebrew', value: 'v0.23.0' })
+  .expectBadge({ label: 'homebrew', message: 'v0.23.0' })
 
 t.create('homebrew (not found)')
   .get('/not-a-package.json')
-  .expectJSON({ name: 'homebrew', value: 'not found' })
+  .expectBadge({ label: 'homebrew', message: 'not found' })
diff --git a/services/hsts/hsts.tester.js b/services/hsts/hsts.tester.js
index 434a1bf2739454fb501b7eb4f31a6ed5a90aa373..6ae2d5ff231c55b4e539efc1c63abf5c336e34b3 100644
--- a/services/hsts/hsts.tester.js
+++ b/services/hsts/hsts.tester.js
@@ -5,25 +5,25 @@ const label = 'hsts preloaded'
 
 t.create('gets the hsts status of github')
   .get('/github.com.json?style=_shields_test')
-  .expectJSON({
-    name: label,
-    value: 'yes',
+  .expectBadge({
+    label,
+    message: 'yes',
     color: 'brightgreen',
   })
 
 t.create('gets the hsts status of httpforever')
   .get('/httpforever.com.json?style=_shields_test')
-  .expectJSON({
-    name: label,
-    value: 'no',
+  .expectBadge({
+    label,
+    message: 'no',
     color: 'red',
   })
 
 t.create('gets the status of an invalid uri')
   .get('/does-not-exist.json?style=_shields_test')
-  .expectJSON({
-    name: label,
-    value: 'no',
+  .expectBadge({
+    label,
+    message: 'no',
     color: 'red',
   })
 
@@ -34,9 +34,9 @@ t.create('gets the hsts status of github (mock)')
       .get('/api/v2/status?domain=github.com')
       .reply(200, { status: 'preloaded' })
   )
-  .expectJSON({
-    name: label,
-    value: 'yes',
+  .expectBadge({
+    label,
+    message: 'yes',
     color: 'brightgreen',
   })
 
@@ -47,9 +47,9 @@ t.create('gets the hsts status of httpforever (mock)')
       .get('/api/v2/status?domain=httpforever.com')
       .reply(200, { status: 'unknown' })
   )
-  .expectJSON({
-    name: label,
-    value: 'no',
+  .expectBadge({
+    label,
+    message: 'no',
     color: 'red',
   })
 
@@ -60,9 +60,9 @@ t.create('gets the hsts status of a pending site (mock)')
       .get('/api/v2/status?domain=pending.mock')
       .reply(200, { status: 'pending' })
   )
-  .expectJSON({
-    name: label,
-    value: 'pending',
+  .expectBadge({
+    label,
+    message: 'pending',
     color: 'yellow',
   })
 
@@ -73,8 +73,8 @@ t.create('gets the status of an invalid uri (mock)')
       .get('/api/v2/status?domain=does-not-exist')
       .reply(200, { status: 'unknown' })
   )
-  .expectJSON({
-    name: label,
-    value: 'no',
+  .expectBadge({
+    label,
+    message: 'no',
     color: 'red',
   })
diff --git a/services/imagelayers/imagelayers.tester.js b/services/imagelayers/imagelayers.tester.js
index e8ce26803b043e9215b50232ef78480779b2b458..35234ace9ed26b87bee947c2d3d8dd73031413fc 100644
--- a/services/imagelayers/imagelayers.tester.js
+++ b/services/imagelayers/imagelayers.tester.js
@@ -9,14 +9,14 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (previously image size)')
   .get('/image-size/_/ubuntu/latest.json')
-  .expectJSON({
-    name: 'imagelayers',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'imagelayers',
+    message: 'no longer available',
   })
 
 t.create('no longer available (previously number of layers)')
   .get('/layers/_/ubuntu/latest.json')
-  .expectJSON({
-    name: 'imagelayers',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'imagelayers',
+    message: 'no longer available',
   })
diff --git a/services/issuestats/issuestats.tester.js b/services/issuestats/issuestats.tester.js
index ea8d5a45673e7fc00bfc7485d019e3a3a9c66da2..1eab93c18a6c516ed0c9991ed7c19f8a7f07fd29 100644
--- a/services/issuestats/issuestats.tester.js
+++ b/services/issuestats/issuestats.tester.js
@@ -7,14 +7,14 @@ module.exports = t
 
 t.create('no longer available (previously issue analysis)')
   .get('/i/github/expressjs/express.json')
-  .expectJSON({
-    name: 'issue stats',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'issue stats',
+    message: 'no longer available',
   })
 
 t.create('no longer available (previously pull request analysis, long form)')
   .get('/p/long/github/expressjs/express.json')
-  .expectJSON({
-    name: 'issue stats',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'issue stats',
+    message: 'no longer available',
   })
diff --git a/services/itunes/itunes.tester.js b/services/itunes/itunes.tester.js
index c1b845536b3269580d294bb07fa4bbb432858755..54a5a8b568f5eb562179c3b4bfb8d44694537a4e 100644
--- a/services/itunes/itunes.tester.js
+++ b/services/itunes/itunes.tester.js
@@ -1,22 +1,19 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('iTunes version (valid)')
   .get('/324684580.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'itunes app store',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'itunes app store',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('iTunes version (not found)')
   .get('/9.json')
-  .expectJSON({ name: 'itunes app store', value: 'not found' })
+  .expectBadge({ label: 'itunes app store', message: 'not found' })
 
 t.create('iTunes version (invalid)')
   .get('/x.json')
-  .expectJSON({ name: 'itunes app store', value: 'invalid' })
+  .expectBadge({ label: 'itunes app store', message: 'invalid' })
diff --git a/services/jenkins/jenkins-coverage.tester.js b/services/jenkins/jenkins-coverage.tester.js
index 9f3926afd793793eaa25ee66d6c621c2bd9e94a1..b6102804f69f4d6a7f707f1c7f9172198445b593 100644
--- a/services/jenkins/jenkins-coverage.tester.js
+++ b/services/jenkins/jenkins-coverage.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isIntegerPercentage } = require('../test-validators')
 
@@ -24,7 +23,7 @@ t.create('jacoco: valid coverage')
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: '81%' })
+  .expectBadge({ label: 'coverage', message: '81%' })
 
 t.create(
   'jacoco: valid coverage (badge URL without leading /job after Jenkins host)'
@@ -41,7 +40,7 @@ t.create(
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: '81%' })
+  .expectBadge({ label: 'coverage', message: '81%' })
 
 t.create('jacoco: invalid data response (no instructionCoverage object)')
   .get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
@@ -56,7 +55,7 @@ t.create('jacoco: invalid data response (no instructionCoverage object)')
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: 'invalid response data' })
+  .expectBadge({ label: 'coverage', message: 'invalid response data' })
 
 t.create('jacoco: invalid data response (non numeric coverage)')
   .get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
@@ -71,11 +70,11 @@ t.create('jacoco: invalid data response (non numeric coverage)')
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: 'invalid response data' })
+  .expectBadge({ label: 'coverage', message: 'invalid response data' })
 
 t.create('jacoco: job not found')
   .get('/j/https/updates.jenkins-ci.org/job/does-not-exist.json')
-  .expectJSON({ name: 'coverage', value: 'job or coverage not found' })
+  .expectBadge({ label: 'coverage', message: 'job or coverage not found' })
 
 t.create('cobertura: valid coverage')
   .get('/c/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
@@ -99,7 +98,7 @@ t.create('cobertura: valid coverage')
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: '64%' })
+  .expectBadge({ label: 'coverage', message: '64%' })
 
 t.create(
   'cobertura: valid coverage (badge URL without leading /job after Jenkins host)'
@@ -125,7 +124,7 @@ t.create(
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: '64%' })
+  .expectBadge({ label: 'coverage', message: '64%' })
 
 t.create('cobertura: invalid data response (non-numeric coverage)')
   .get('/c/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
@@ -145,7 +144,7 @@ t.create('cobertura: invalid data response (non-numeric coverage)')
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: 'invalid response data' })
+  .expectBadge({ label: 'coverage', message: 'invalid response data' })
 
 t.create('cobertura: invalid data response (missing line coverage)')
   .get('/c/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
@@ -165,15 +164,13 @@ t.create('cobertura: invalid data response (missing line coverage)')
         },
       })
   )
-  .expectJSON({ name: 'coverage', value: 'invalid response data' })
+  .expectBadge({ label: 'coverage', message: 'invalid response data' })
 
 t.create('cobertura: job not found')
   .get('/c/https/updates.jenkins-ci.org/job/does-not-exist.json')
-  .expectJSON({ name: 'coverage', value: 'job or coverage not found' })
+  .expectBadge({ label: 'coverage', message: 'job or coverage not found' })
 
 t.create('cobertura: job found')
   .get('/c/https/builds.apache.org/job/olingo-odata4-cobertura.json')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'coverage', value: isIntegerPercentage })
-  )
+  .expectBadge({ label: 'coverage', message: isIntegerPercentage })
diff --git a/services/jenkins/jenkins-plugin-installs.tester.js b/services/jenkins/jenkins-plugin-installs.tester.js
index 231f7ee1d04b5a8186152b6b24c5441396197063..5c9329dbed6a377317d4227de539b2e34129a8d3 100644
--- a/services/jenkins/jenkins-plugin-installs.tester.js
+++ b/services/jenkins/jenkins-plugin-installs.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -8,41 +7,35 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('total installs | valid')
   .get('/view-job-filters.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: isMetric,
+  })
 
 t.create('total installs | not found')
   .get('/not-a-plugin.json')
-  .expectJSON({ name: 'installs', value: 'plugin not found' })
+  .expectBadge({ label: 'installs', message: 'plugin not found' })
 
 // version installs
 
 t.create('version installs | valid: numeric version')
   .get('/view-job-filters/1.26.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs@1.26',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs@1.26',
+    message: isMetric,
+  })
 
 t.create('version installs | valid: alphanumeric version')
   .get('/view-job-filters/1.27-DRE1.00.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs@1.27-DRE1.00',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs@1.27-DRE1.00',
+    message: isMetric,
+  })
 
 t.create('version installs | not found: non-existent plugin')
   .get('/not-a-plugin/1.26.json')
-  .expectJSON({ name: 'installs', value: 'plugin not found' })
+  .expectBadge({ label: 'installs', message: 'plugin not found' })
 
 t.create('version installs | not found: non-existent version')
   .get('/view-job-filters/1.1-NOT-FOUND.json')
-  .expectJSON({ name: 'installs', value: 'version not found' })
+  .expectBadge({ label: 'installs', message: 'version not found' })
diff --git a/services/jenkins/jenkins-plugin-version.tester.js b/services/jenkins/jenkins-plugin-version.tester.js
index fb35de98210b987582b40dec18ed84251c20925f..c33a2e563fb1c8bfe521d2aa39bb42511ff4a7cc 100644
--- a/services/jenkins/jenkins-plugin-version.tester.js
+++ b/services/jenkins/jenkins-plugin-version.tester.js
@@ -17,12 +17,10 @@ t.create('latest version')
       .get('/current/update-center.actual.json')
       .reply(200, { plugins: { blueocean: { version: '1.1.6' } } })
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'plugin',
-      value: Joi.string().regex(/^v(.*)$/),
-    })
-  )
+  .expectBadge({
+    label: 'plugin',
+    message: Joi.string().regex(/^v(.*)$/),
+  })
 
 t.create('version 0')
   .get('/plugin/v/blueocean.json')
@@ -31,12 +29,10 @@ t.create('version 0')
       .get('/current/update-center.actual.json')
       .reply(200, { plugins: { blueocean: { version: '0' } } })
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'plugin',
-      value: Joi.string().regex(/^v0$/),
-    })
-  )
+  .expectBadge({
+    label: 'plugin',
+    message: Joi.string().regex(/^v0$/),
+  })
 
 t.create('inexistent artifact')
   .get('/plugin/v/inexistent-artifact-id.json')
@@ -45,9 +41,9 @@ t.create('inexistent artifact')
       .get('/current/update-center.actual.json')
       .reply(200, { plugins: { blueocean: { version: '1.1.6' } } })
   )
-  .expectJSON({ name: 'plugin', value: 'not found' })
+  .expectBadge({ label: 'plugin', message: 'not found' })
 
 t.create('connection error')
   .get('/plugin/v/blueocean.json')
   .networkOff()
-  .expectJSON({ name: 'plugin', value: 'inaccessible' })
+  .expectBadge({ label: 'plugin', message: 'inaccessible' })
diff --git a/services/jetbrains/jetbrains.tester.js b/services/jetbrains/jetbrains.tester.js
index a0fc53dd741c6951d3a191999b7ab3390e055906..691d2f1feaa09007059ad928175ddf8cb211e6c1 100644
--- a/services/jetbrains/jetbrains.tester.js
+++ b/services/jetbrains/jetbrains.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric, isVPlusDottedVersionNClauses } = require('../test-validators')
 
@@ -9,15 +8,15 @@ module.exports = t
 
 t.create('downloads (number as a plugin id)')
   .get('/plugin/d/7495.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('downloads (plugin id from plugin.xml)')
   .get('/plugin/d/org.intellij.scala.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('downloads (user friendly plugin id)')
   .get('/plugin/d/1347-scala.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('downloads (mocked)')
   .get('/plugin/d/9435.json')
@@ -38,16 +37,16 @@ t.create('downloads (mocked)')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'downloads', value: '2' })
+  .expectBadge({ label: 'downloads', message: '2' })
 
 t.create('unknown plugin')
   .get('/plugin/d/unknown-plugin.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 t.create('connection error')
   .get('/plugin/d/7495.json')
   .networkOff()
-  .expectJSON({ name: 'downloads', value: 'inaccessible' })
+  .expectBadge({ label: 'downloads', message: 'inaccessible' })
 
 t.create('server error')
   .get('/plugin/d/7495.json')
@@ -56,7 +55,7 @@ t.create('server error')
       .get('/plugins/list?pluginId=7495')
       .reply(500)
   )
-  .expectJSON({ name: 'downloads', value: 'inaccessible' })
+  .expectBadge({ label: 'downloads', message: 'inaccessible' })
 
 t.create('empty response')
   .get('/plugin/d/7495.json')
@@ -65,7 +64,7 @@ t.create('empty response')
       .get('/plugins/list?pluginId=7495')
       .reply(200, '')
   )
-  .expectJSON({ name: 'downloads', value: 'unparseable xml response' })
+  .expectBadge({ label: 'downloads', message: 'unparseable xml response' })
 
 t.create('incorrect response format (JSON instead of XML)')
   .get('/plugin/d/7495.json')
@@ -74,7 +73,7 @@ t.create('incorrect response format (JSON instead of XML)')
       .get('/plugins/list?pluginId=7495')
       .reply(200, { downloads: 2 })
   )
-  .expectJSON({ name: 'downloads', value: 'unparseable xml response' })
+  .expectBadge({ label: 'downloads', message: 'unparseable xml response' })
 
 t.create('missing required XML element')
   .get('/plugin/d/9435.json')
@@ -96,7 +95,7 @@ t.create('missing required XML element')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'downloads', value: 'invalid response data' })
+  .expectBadge({ label: 'downloads', message: 'invalid response data' })
 
 t.create('missing required XML attribute')
   .get('/plugin/d/9435.json')
@@ -129,7 +128,7 @@ t.create('missing required XML attribute')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'downloads', value: 'invalid response data' })
+  .expectBadge({ label: 'downloads', message: 'invalid response data' })
 
 t.create('empty XML')
   .get('/plugin/d/9435.json')
@@ -142,7 +141,7 @@ t.create('empty XML')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'downloads', value: 'unparseable xml response' })
+  .expectBadge({ label: 'downloads', message: 'unparseable xml response' })
 
 t.create('XML with unknown root')
   .get('/plugin/d/9435.json')
@@ -155,7 +154,7 @@ t.create('XML with unknown root')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'downloads', value: 'invalid response data' })
+  .expectBadge({ label: 'downloads', message: 'invalid response data' })
 
 t.create('404 status code')
   .get('/plugin/d/7495.json')
@@ -164,7 +163,7 @@ t.create('404 status code')
       .get('/plugins/list?pluginId=7495')
       .reply(404)
   )
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 t.create('empty XML(v)')
   .get('/plugin/v/9435.json')
@@ -177,7 +176,10 @@ t.create('empty XML(v)')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'unparseable xml response' })
+  .expectBadge({
+    label: 'jetbrains plugin',
+    message: 'unparseable xml response',
+  })
 
 t.create('404 status code(v)')
   .get('/plugin/v/7495.json')
@@ -186,7 +188,7 @@ t.create('404 status code(v)')
       .get('/plugins/list?pluginId=7495')
       .reply(404)
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'not found' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'not found' })
 
 t.create('missing required XML element(v)')
   .get('/plugin/v/9435.json')
@@ -208,7 +210,7 @@ t.create('missing required XML element(v)')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'invalid response data' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'invalid response data' })
 
 t.create('incorrect response format (JSON instead of XML)(v)')
   .get('/plugin/v/7495.json')
@@ -217,7 +219,10 @@ t.create('incorrect response format (JSON instead of XML)(v)')
       .get('/plugins/list?pluginId=7495')
       .reply(200, { version: 2.0 })
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'unparseable xml response' })
+  .expectBadge({
+    label: 'jetbrains plugin',
+    message: 'unparseable xml response',
+  })
 
 t.create('empty response(v)')
   .get('/plugin/v/7495.json')
@@ -226,7 +231,10 @@ t.create('empty response(v)')
       .get('/plugins/list?pluginId=7495')
       .reply(200, '')
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'unparseable xml response' })
+  .expectBadge({
+    label: 'jetbrains plugin',
+    message: 'unparseable xml response',
+  })
 
 t.create('server error(v)')
   .get('/plugin/v/7495.json')
@@ -235,43 +243,37 @@ t.create('server error(v)')
       .get('/plugins/list?pluginId=7495')
       .reply(500)
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'inaccessible' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'inaccessible' })
 
 t.create('connection error(v)')
   .get('/plugin/v/7495.json')
   .networkOff()
-  .expectJSON({ name: 'jetbrains plugin', value: 'inaccessible' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'inaccessible' })
 
 t.create('version for unknown plugin')
   .get('/plugin/v/unknown-plugin.json')
-  .expectJSON({ name: 'jetbrains plugin', value: 'not found' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'not found' })
 
 t.create('version (user friendly plugin id)')
   .get('/plugin/v/1347-scala.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jetbrains plugin',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'jetbrains plugin',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (plugin id from plugin.xml)')
   .get('/plugin/v/org.intellij.scala.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jetbrains plugin',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'jetbrains plugin',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (number as a plugin id)')
   .get('/plugin/v/7495.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jetbrains plugin',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'jetbrains plugin',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (mocked)')
   .get('/plugin/v/9435.json')
@@ -294,7 +296,7 @@ t.create('version (mocked)')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'v1.0' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'v1.0' })
 
 t.create('XML with unknown root (v)')
   .get('/plugin/v/9435.json')
@@ -307,4 +309,4 @@ t.create('XML with unknown root (v)')
       'Content-Type': 'text/xml;charset=UTF-8',
     }
   )
-  .expectJSON({ name: 'jetbrains plugin', value: 'invalid response data' })
+  .expectBadge({ label: 'jetbrains plugin', message: 'invalid response data' })
diff --git a/services/jira/jira-issue.tester.js b/services/jira/jira-issue.tester.js
index 44d4c84c08c73d16190529c2d8fe6f5da58c518b..020fb55e14a9d001b2b63992f8d9c3ef12316ff9 100644
--- a/services/jira/jira-issue.tester.js
+++ b/services/jira/jira-issue.tester.js
@@ -5,11 +5,11 @@ const { mockJiraCreds, restore, user, pass } = require('./jira-test-helpers')
 
 t.create('live: unknown issue')
   .get('/https/issues.apache.org/jira/notArealIssue-000.json')
-  .expectJSON({ name: 'jira', value: 'issue not found' })
+  .expectBadge({ label: 'jira', message: 'issue not found' })
 
 t.create('live: known issue')
   .get('/https/issues.apache.org/jira/kafka-2896.json')
-  .expectJSON({ name: 'kafka-2896', value: 'Resolved' })
+  .expectBadge({ label: 'kafka-2896', message: 'Resolved' })
 
 t.create('no status color')
   .get('/http/issues.apache.org/jira/foo-123.json?style=_shields_test')
@@ -24,9 +24,9 @@ t.create('no status color')
         },
       })
   )
-  .expectJSON({
-    name: 'foo-123',
-    value: 'pending',
+  .expectBadge({
+    label: 'foo-123',
+    message: 'pending',
     color: 'lightgrey',
   })
 
@@ -46,9 +46,9 @@ t.create('green status color')
         },
       })
   )
-  .expectJSON({
-    name: 'bar-345',
-    value: 'done',
+  .expectBadge({
+    label: 'bar-345',
+    message: 'done',
     color: 'green',
   })
 
@@ -68,9 +68,9 @@ t.create('medium-gray status color')
         },
       })
   )
-  .expectJSON({
-    name: 'abc-123',
-    value: 'under review',
+  .expectBadge({
+    label: 'abc-123',
+    message: 'under review',
     color: 'lightgrey',
   })
 
@@ -90,9 +90,9 @@ t.create('yellow status color')
         },
       })
   )
-  .expectJSON({
-    name: 'test-001',
-    value: 'in progress',
+  .expectBadge({
+    label: 'test-001',
+    message: 'in progress',
     color: 'yellow',
   })
 
@@ -112,9 +112,9 @@ t.create('brown status color')
         },
       })
   )
-  .expectJSON({
-    name: 'zzz-789',
-    value: 'muddy',
+  .expectBadge({
+    label: 'zzz-789',
+    message: 'muddy',
     color: 'orange',
   })
 
@@ -134,9 +134,9 @@ t.create('warm-red status color')
         },
       })
   )
-  .expectJSON({
-    name: 'fire-321',
-    value: 'heating up',
+  .expectBadge({
+    label: 'fire-321',
+    message: 'heating up',
     color: 'red',
   })
 
@@ -156,9 +156,9 @@ t.create('blue-gray status color')
         },
       })
   )
-  .expectJSON({
-    name: 'sky-775',
-    value: 'cloudy',
+  .expectBadge({
+    label: 'sky-775',
+    message: 'cloudy',
     color: 'blue',
   })
 
@@ -183,4 +183,4 @@ t.create('with mock credentials')
       })
   )
   .finally(restore)
-  .expectJSON({ name: 'secure-234', value: 'in progress' })
+  .expectBadge({ label: 'secure-234', message: 'in progress' })
diff --git a/services/jira/jira-sprint.tester.js b/services/jira/jira-sprint.tester.js
index 6bab3b7c85fd512f6034cb7e4b8d9d2459d15e90..b75738851a28c4cdf4d93a25a37bd3d7c95634a6 100644
--- a/services/jira/jira-sprint.tester.js
+++ b/services/jira/jira-sprint.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { isIntegerPercentage } = require('../test-validators')
 const { mockJiraCreds, restore, user, pass } = require('./jira-test-helpers')
@@ -14,16 +13,14 @@ const queryString = {
 
 t.create('live: unknown sprint')
   .get('/https/jira.spring.io/abc.json')
-  .expectJSON({ name: 'jira', value: 'sprint not found' })
+  .expectBadge({ label: 'jira', message: 'sprint not found' })
 
 t.create('live: known sprint')
   .get('/https/jira.spring.io/94.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'completion',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'completion',
+    message: isIntegerPercentage,
+  })
 
 t.create('100% completion')
   .get(`/http/issues.apache.org/jira/${sprintId}.json?style=_shields_test`)
@@ -51,9 +48,9 @@ t.create('100% completion')
         ],
       })
   )
-  .expectJSON({
-    name: 'completion',
-    value: '100%',
+  .expectBadge({
+    label: 'completion',
+    message: '100%',
     color: 'brightgreen',
   })
 
@@ -76,9 +73,9 @@ t.create('0% completion')
         ],
       })
   )
-  .expectJSON({
-    name: 'completion',
-    value: '0%',
+  .expectBadge({
+    label: 'completion',
+    message: '0%',
     color: 'red',
   })
 
@@ -93,9 +90,9 @@ t.create('no issues in sprint')
         issues: [],
       })
   )
-  .expectJSON({
-    name: 'completion',
-    value: '0%',
+  .expectBadge({
+    label: 'completion',
+    message: '0%',
     color: 'red',
   })
 
@@ -123,9 +120,9 @@ t.create('issue with null resolution value')
         ],
       })
   )
-  .expectJSON({
-    name: 'completion',
-    value: '50%',
+  .expectBadge({
+    label: 'completion',
+    message: '50%',
     color: 'orange',
   })
 
@@ -163,4 +160,4 @@ t.create('with mock credentials')
       })
   )
   .finally(restore)
-  .expectJSON({ name: 'completion', value: '50%' })
+  .expectBadge({ label: 'completion', message: '50%' })
diff --git a/services/jitpack/jitpack.tester.js b/services/jitpack/jitpack.tester.js
index ecd18b20586d8aab5d4801b7b708eb4953814873..e558381e519b7158d2514a35c7b0daab34e92164 100644
--- a/services/jitpack/jitpack.tester.js
+++ b/services/jitpack/jitpack.tester.js
@@ -8,8 +8,8 @@ const isAnyV = Joi.string().regex(/^v.+$/)
 
 t.create('version')
   .get('/jitpack/maven-simple.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'jitpack', value: isAnyV }))
+  .expectBadge({ label: 'jitpack', message: isAnyV })
 
 t.create('unknown package')
   .get('/some-bogus-user/project.json')
-  .expectJSON({ name: 'jitpack', value: 'project not found or private' })
+  .expectBadge({ label: 'jitpack', message: 'project not found or private' })
diff --git a/services/jsdelivr/jsdelivr-hits-github.tester.js b/services/jsdelivr/jsdelivr-hits-github.tester.js
index b34a89582e9b4d21847255a77e14af8616f4be5c..04256b4e8563e77baf4a0d3f0da4f7d18f50e1b1 100644
--- a/services/jsdelivr/jsdelivr-hits-github.tester.js
+++ b/services/jsdelivr/jsdelivr-hits-github.tester.js
@@ -1,49 +1,40 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetricOverTimePeriod } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('(live) jquery/jquery hits/day')
   .get('/hd/jquery/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) jquery/jquery hits/week')
   .get('/hw/jquery/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) jquery/jquery hits/month')
   .get('/hm/jquery/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) jquery/jquery hits/year')
   .get('/hy/jquery/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) fake package')
   .get('/hd/somefakepackage/somefakepackage.json')
-  .expectJSON({
-    name: 'jsdelivr',
+  .expectBadge({
+    label: 'jsdelivr',
     // Will return 0 hits/day as the endpoint can't send 404s at present.
-    value: '0/day',
+    message: '0/day',
   })
diff --git a/services/jsdelivr/jsdelivr-hits-npm.tester.js b/services/jsdelivr/jsdelivr-hits-npm.tester.js
index 8ecf7952accb46aaccb5e27434419287a674f75a..2e1ed411991c580dfd9ce0a361041c16571102f0 100644
--- a/services/jsdelivr/jsdelivr-hits-npm.tester.js
+++ b/services/jsdelivr/jsdelivr-hits-npm.tester.js
@@ -1,49 +1,40 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetricOverTimePeriod } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('(live) jquery hits/day')
   .get('/hd/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) jquery hits/week')
   .get('/hw/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) jquery hits/month')
   .get('/hm/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) jquery hits/year')
   .get('/hy/jquery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'jsdelivr',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'jsdelivr',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('(live) fake package')
   .get('/hd/somefakepackage.json')
-  .expectJSON({
-    name: 'jsdelivr',
+  .expectBadge({
+    label: 'jsdelivr',
     // Will return 0 hits/day as the endpoint can't send 404s at present.
-    value: '0/day',
+    message: '0/day',
   })
diff --git a/services/keybase/keybase-btc.tester.js b/services/keybase/keybase-btc.tester.js
index c9f4ba48b435791b9dfb154aae15363061b0e603..325203385b0600aa8e99984f65140b88c8df2635 100644
--- a/services/keybase/keybase-btc.tester.js
+++ b/services/keybase/keybase-btc.tester.js
@@ -1,41 +1,32 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('existing bitcoin address')
   .get('/skyplabs.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'btc',
-      value: withRegex(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/),
-    })
-  )
+  .expectBadge({
+    label: 'btc',
+    message: withRegex(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/),
+  })
 
 t.create('unknown username')
   .get('/skyplabsssssss.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'btc',
-      value: 'profile not found',
-    })
-  )
+  .expectBadge({
+    label: 'btc',
+    message: 'profile not found',
+  })
 
 t.create('invalid username')
   .get('/s.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'btc',
-      value: 'invalid username',
-    })
-  )
+  .expectBadge({
+    label: 'btc',
+    message: 'invalid username',
+  })
 
 t.create('missing bitcoin address')
   .get('/test.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'btc',
-      value: 'no bitcoin addresses found',
-    })
-  )
+  .expectBadge({
+    label: 'btc',
+    message: 'no bitcoin addresses found',
+  })
diff --git a/services/keybase/keybase-pgp.tester.js b/services/keybase/keybase-pgp.tester.js
index 99baecaefbde1bb50152af21a06adf6a01b05ada..ecc1388a76f26b9522a06a82944323815ffcb901 100644
--- a/services/keybase/keybase-pgp.tester.js
+++ b/services/keybase/keybase-pgp.tester.js
@@ -5,38 +5,30 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('existing key fingerprint')
   .get('/skyplabs.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'pgp',
-      value: Joi.string()
-        .hex()
-        .length(16),
-    })
-  )
+  .expectBadge({
+    label: 'pgp',
+    message: Joi.string()
+      .hex()
+      .length(16),
+  })
 
 t.create('unknown username')
   .get('/skyplabsssssss.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'pgp',
-      value: 'profile not found',
-    })
-  )
+  .expectBadge({
+    label: 'pgp',
+    message: 'profile not found',
+  })
 
 t.create('invalid username')
   .get('/s.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'pgp',
-      value: 'invalid username',
-    })
-  )
+  .expectBadge({
+    label: 'pgp',
+    message: 'invalid username',
+  })
 
 t.create('missing key fingerprint')
   .get('/skyp.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'pgp',
-      value: 'no key fingerprint found',
-    })
-  )
+  .expectBadge({
+    label: 'pgp',
+    message: 'no key fingerprint found',
+  })
diff --git a/services/keybase/keybase-xlm.tester.js b/services/keybase/keybase-xlm.tester.js
index 083bdf39f151950d0d8fbd27bb5f0d5e4d48410a..839e8348a402097cc6e7f5068fb55f37e4d39a97 100644
--- a/services/keybase/keybase-xlm.tester.js
+++ b/services/keybase/keybase-xlm.tester.js
@@ -1,41 +1,32 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('existing stellar address')
   .get('/skyplabs.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'xlm',
-      value: withRegex(/^(?!not found$)/),
-    })
-  )
+  .expectBadge({
+    label: 'xlm',
+    message: withRegex(/^(?!not found$)/),
+  })
 
 t.create('unknown username')
   .get('/skyplabsssssss.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'xlm',
-      value: 'profile not found',
-    })
-  )
+  .expectBadge({
+    label: 'xlm',
+    message: 'profile not found',
+  })
 
 t.create('invalid username')
   .get('/s.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'xlm',
-      value: 'invalid username',
-    })
-  )
+  .expectBadge({
+    label: 'xlm',
+    message: 'invalid username',
+  })
 
 t.create('missing stellar address')
   .get('/test.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'xlm',
-      value: 'no stellar address found',
-    })
-  )
+  .expectBadge({
+    label: 'xlm',
+    message: 'no stellar address found',
+  })
diff --git a/services/keybase/keybase-zec.tester.js b/services/keybase/keybase-zec.tester.js
index 82064ea080e179227717eff4c233dd2086db5630..7d6bfbd7dd611954394637ab7fe640a280954499 100644
--- a/services/keybase/keybase-zec.tester.js
+++ b/services/keybase/keybase-zec.tester.js
@@ -1,41 +1,32 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('existing zcash address')
   .get('/skyplabs.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'zec',
-      value: withRegex(/^(?!not found$)/),
-    })
-  )
+  .expectBadge({
+    label: 'zec',
+    message: withRegex(/^(?!not found$)/),
+  })
 
 t.create('unknown username')
   .get('/skyplabsssssss.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'zec',
-      value: 'profile not found',
-    })
-  )
+  .expectBadge({
+    label: 'zec',
+    message: 'profile not found',
+  })
 
 t.create('invalid username')
   .get('/s.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'zec',
-      value: 'invalid username',
-    })
-  )
+  .expectBadge({
+    label: 'zec',
+    message: 'invalid username',
+  })
 
 t.create('missing zcash address')
   .get('/test.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'zec',
-      value: 'no zcash addresses found',
-    })
-  )
+  .expectBadge({
+    label: 'zec',
+    message: 'no zcash addresses found',
+  })
diff --git a/services/leanpub/leanpub-book-summary.tester.js b/services/leanpub/leanpub-book-summary.tester.js
index 0fb5154d86a780701cd8eaa91e48af6a776117c8..8c019fea97c1122dd9e77e4ae4cbde6755d7f51d 100644
--- a/services/leanpub/leanpub-book-summary.tester.js
+++ b/services/leanpub/leanpub-book-summary.tester.js
@@ -7,25 +7,21 @@ const knownValidBook = 'juice-shop'
 
 t.create('known book pages')
   .get(`/pages/${knownValidBook}.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pages',
-      value: Joi.number(),
-    })
-  )
+  .expectBadge({
+    label: 'pages',
+    message: Joi.number(),
+  })
 
 t.create('known book sold')
   .get(`/sold/${knownValidBook}.json`)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'sold',
-      value: Joi.number(),
-    })
-  )
+  .expectBadge({
+    label: 'sold',
+    message: Joi.number(),
+  })
 
 t.create('unknown book')
   .get(`/pages/234uFjAsDf234209.json`)
-  .expectJSON({ name: 'leanpub', value: 'book not found' })
+  .expectBadge({ label: 'leanpub', message: 'book not found' })
 
 t.create('404 book summary error response')
   .get(`/pages/${knownValidBook}.json`)
@@ -34,9 +30,9 @@ t.create('404 book summary error response')
       .get(`/${knownValidBook}.json`)
       .reply(404)
   )
-  .expectJSON({
-    name: 'leanpub',
-    value: 'book not found',
+  .expectBadge({
+    label: 'leanpub',
+    message: 'book not found',
   })
 
 t.create('correct page count')
@@ -50,9 +46,9 @@ t.create('correct page count')
         total_copies_sold: 27,
       })
   )
-  .expectJSON({
-    name: 'pages',
-    value: '190',
+  .expectBadge({
+    label: 'pages',
+    message: '190',
   })
 
 t.create('correct sold count')
@@ -66,7 +62,7 @@ t.create('correct sold count')
         total_copies_sold: 82347,
       })
   )
-  .expectJSON({
-    name: 'sold',
-    value: '82347',
+  .expectBadge({
+    label: 'sold',
+    message: '82347',
   })
diff --git a/services/lgtm/lgtm.tester.js b/services/lgtm/lgtm.tester.js
index 7dafca2da6b98a588c5a43b8475a1831280253cc..a0ac11bcf3fb7875172eb1e4636d471c868ca8de 100644
--- a/services/lgtm/lgtm.tester.js
+++ b/services/lgtm/lgtm.tester.js
@@ -9,18 +9,16 @@ module.exports = t
 
 t.create('alerts: total alerts for a project')
   .get('/alerts/g/apache/cloudstack.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'lgtm',
-      value: Joi.string().regex(/^[0-9kM.]+ alerts?$/),
-    })
-  )
+  .expectBadge({
+    label: 'lgtm',
+    message: Joi.string().regex(/^[0-9kM.]+ alerts?$/),
+  })
 
 t.create('alerts: missing project')
   .get('/alerts/g/some-org/this-project-doesnt-exist.json')
-  .expectJSON({
-    name: 'lgtm',
-    value: 'project not found',
+  .expectBadge({
+    label: 'lgtm',
+    message: 'project not found',
   })
 
 t.create('alerts: no alerts')
@@ -30,7 +28,7 @@ t.create('alerts: no alerts')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, { alerts: 0, languages: data.languages })
   )
-  .expectJSON({ name: 'lgtm', value: '0 alerts' })
+  .expectBadge({ label: 'lgtm', message: '0 alerts' })
 
 t.create('alerts: single alert')
   .get('/alerts/g/apache/cloudstack.json')
@@ -39,7 +37,7 @@ t.create('alerts: single alert')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, { alerts: 1, languages: data.languages })
   )
-  .expectJSON({ name: 'lgtm', value: '1 alert' })
+  .expectBadge({ label: 'lgtm', message: '1 alert' })
 
 t.create('alerts: multiple alerts')
   .get('/alerts/g/apache/cloudstack.json')
@@ -48,7 +46,7 @@ t.create('alerts: multiple alerts')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, { alerts: 123, languages: data.languages })
   )
-  .expectJSON({ name: 'lgtm', value: '123 alerts' })
+  .expectBadge({ label: 'lgtm', message: '123 alerts' })
 
 t.create('alerts: json missing alerts')
   .get('/alerts/g/apache/cloudstack.json')
@@ -57,15 +55,15 @@ t.create('alerts: json missing alerts')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, {})
   )
-  .expectJSON({ name: 'lgtm', value: 'invalid response data' })
+  .expectBadge({ label: 'lgtm', message: 'invalid response data' })
 
 // Grade Badge
 
 t.create('grade: missing project')
   .get('/grade/java/g/some-org/this-project-doesnt-exist.json')
-  .expectJSON({
-    name: 'lgtm',
-    value: 'project not found',
+  .expectBadge({
+    label: 'lgtm',
+    message: 'project not found',
   })
 
 t.create('grade: json missing languages')
@@ -75,22 +73,20 @@ t.create('grade: json missing languages')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, {})
   )
-  .expectJSON({ name: 'lgtm', value: 'invalid response data' })
+  .expectBadge({ label: 'lgtm', message: 'invalid response data' })
 
 t.create('grade: grade for a project (java)')
   .get('/grade/java/g/apache/cloudstack.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'code quality: java',
-      value: Joi.string().regex(/^(?:A\+)|A|B|C|D|E$/),
-    })
-  )
+  .expectBadge({
+    label: 'code quality: java',
+    message: Joi.string().regex(/^(?:A\+)|A|B|C|D|E$/),
+  })
 
 t.create('grade: grade for missing language')
   .get('/grade/foo/g/apache/cloudstack.json')
-  .expectJSON({
-    name: 'code quality: foo',
-    value: 'no language data',
+  .expectBadge({
+    label: 'code quality: foo',
+    message: 'no language data',
   })
 
 // Test display of languages
@@ -115,7 +111,7 @@ t.create('grade: cpp')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: c/c++', value: 'A+' })
+  .expectBadge({ label: 'code quality: c/c++', message: 'A+' })
 
 t.create('grade: javascript')
   .get('/grade/javascript/g/apache/cloudstack.json')
@@ -124,7 +120,7 @@ t.create('grade: javascript')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: js/ts', value: 'A' })
+  .expectBadge({ label: 'code quality: js/ts', message: 'A' })
 
 t.create('grade: java')
   .get('/grade/java/g/apache/cloudstack.json')
@@ -133,7 +129,7 @@ t.create('grade: java')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: java', value: 'B' })
+  .expectBadge({ label: 'code quality: java', message: 'B' })
 
 t.create('grade: python')
   .get('/grade/python/g/apache/cloudstack.json')
@@ -142,7 +138,7 @@ t.create('grade: python')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: python', value: 'C' })
+  .expectBadge({ label: 'code quality: python', message: 'C' })
 
 t.create('grade: csharp')
   .get('/grade/csharp/g/apache/cloudstack.json')
@@ -151,7 +147,7 @@ t.create('grade: csharp')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: c#', value: 'D' })
+  .expectBadge({ label: 'code quality: c#', message: 'D' })
 
 t.create('grade: other')
   .get('/grade/other/g/apache/cloudstack.json')
@@ -160,7 +156,7 @@ t.create('grade: other')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: other', value: 'E' })
+  .expectBadge({ label: 'code quality: other', message: 'E' })
 
 t.create('grade: foo (no grade for valid language)')
   .get('/grade/foo/g/apache/cloudstack.json')
@@ -169,4 +165,4 @@ t.create('grade: foo (no grade for valid language)')
       .get('/api/v0.1/project/g/apache/cloudstack/details')
       .reply(200, data)
   )
-  .expectJSON({ name: 'code quality: foo', value: 'no language data' })
+  .expectBadge({ label: 'code quality: foo', message: 'no language data' })
diff --git a/services/liberapay/liberapay-gives.tester.js b/services/liberapay/liberapay-gives.tester.js
index ce47137dfd6508571b55dbc23efe4240caad9f99..1faec4be96dfcddde8a887684c1717a9b4fefbb9 100644
--- a/services/liberapay/liberapay-gives.tester.js
+++ b/services/liberapay/liberapay-gives.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { isCurrencyOverTime } = require('./liberapay-base')
 
 t.create('Giving (valid)')
   .get('/Changaco.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'gives',
-      value: isCurrencyOverTime,
-    })
-  )
+  .expectBadge({
+    label: 'gives',
+    message: isCurrencyOverTime,
+  })
 
 t.create('Giving (not found)')
   .get('/does-not-exist.json')
-  .expectJSON({ name: 'liberapay', value: 'not found' })
+  .expectBadge({ label: 'liberapay', message: 'not found' })
 
 t.create('Giving (null)')
   .get('/Liberapay.json')
@@ -29,4 +26,4 @@ t.create('Giving (null)')
         goal: null,
       })
   )
-  .expectJSON({ name: 'liberapay', value: 'no public giving stats' })
+  .expectBadge({ label: 'liberapay', message: 'no public giving stats' })
diff --git a/services/liberapay/liberapay-goal.tester.js b/services/liberapay/liberapay-goal.tester.js
index 2b44734f00301fd6cd5852dcce10cbe7456990ca..42189d70ccc27f8293a1b34da6180e70ab8b5fc0 100644
--- a/services/liberapay/liberapay-goal.tester.js
+++ b/services/liberapay/liberapay-goal.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Goal Progress (valid)')
   .get('/Liberapay.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'goal progress',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'goal progress',
+    message: isIntegerPercentage,
+  })
 
 t.create('Goal Progress (not found)')
   .get('/does-not-exist.json')
-  .expectJSON({ name: 'liberapay', value: 'not found' })
+  .expectBadge({ label: 'liberapay', message: 'not found' })
 
 t.create('Goal Progress (no goal set)')
   .get('/Liberapay.json')
@@ -29,4 +26,4 @@ t.create('Goal Progress (no goal set)')
         goal: null,
       })
   )
-  .expectJSON({ name: 'liberapay', value: 'no public goals' })
+  .expectBadge({ label: 'liberapay', message: 'no public goals' })
diff --git a/services/liberapay/liberapay-patrons.tester.js b/services/liberapay/liberapay-patrons.tester.js
index 6bd0b598d7c8c08ab6c0eb30abcc69319691228a..d6e78b788cc21580ce00005af52ac70affc7dea2 100644
--- a/services/liberapay/liberapay-patrons.tester.js
+++ b/services/liberapay/liberapay-patrons.tester.js
@@ -1,18 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Patrons (valid)')
   .get('/Liberapay.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'patrons',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'patrons',
+    message: isMetric,
+  })
 
 t.create('Patrons (not found)')
   .get('/does-not-exist.json')
-  .expectJSON({ name: 'liberapay', value: 'not found' })
+  .expectBadge({ label: 'liberapay', message: 'not found' })
diff --git a/services/liberapay/liberapay-receives.tester.js b/services/liberapay/liberapay-receives.tester.js
index 38fdbf8756e82c4a0f5c8750f817f26f86cdecc7..3ae69dfff644e88f7ac5ef2764489fab05beeffe 100644
--- a/services/liberapay/liberapay-receives.tester.js
+++ b/services/liberapay/liberapay-receives.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { isCurrencyOverTime } = require('./liberapay-base')
 
 t.create('Receiving (valid)')
   .get('/Changaco.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'receives',
-      value: isCurrencyOverTime,
-    })
-  )
+  .expectBadge({
+    label: 'receives',
+    message: isCurrencyOverTime,
+  })
 
 t.create('Receiving (not found)')
   .get('/does-not-exist.json')
-  .expectJSON({ name: 'liberapay', value: 'not found' })
+  .expectBadge({ label: 'liberapay', message: 'not found' })
 
 t.create('Receiving (null)')
   .get('/Liberapay.json')
@@ -29,4 +26,4 @@ t.create('Receiving (null)')
         goal: null,
       })
   )
-  .expectJSON({ name: 'liberapay', value: 'no public receiving stats' })
+  .expectBadge({ label: 'liberapay', message: 'no public receiving stats' })
diff --git a/services/librariesio/librariesio-dependencies.tester.js b/services/librariesio/librariesio-dependencies.tester.js
index 289c24017b673dea410a1d7acb38d0e51e8e883e..e02981a279a9d7fc722652d232585f83c104178c 100644
--- a/services/librariesio/librariesio-dependencies.tester.js
+++ b/services/librariesio/librariesio-dependencies.tester.js
@@ -1,41 +1,34 @@
 'use strict'
 
-const Joi = require('joi')
 const { isDependencyState } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('dependencies for releases')
   .get('/release/hex/phoenix/1.0.3.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: isDependencyState,
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: isDependencyState,
+  })
 
 t.create('dependencies for releases (project name contains dot)')
   .get('/release/nuget/Newtonsoft.Json.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: isDependencyState,
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: isDependencyState,
+  })
 
 t.create('dependencies for github')
   .get('/github/pyvesb/notepad4e.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependencies',
-      value: isDependencyState,
-    })
-  )
+  .expectBadge({
+    label: 'dependencies',
+    message: isDependencyState,
+  })
 
 t.create('release not found')
   .get('/release/hex/invalid/4.0.4.json')
-  .expectJSON({
-    name: 'dependencies',
-    value: 'not available',
+  .expectBadge({
+    label: 'dependencies',
+    message: 'not available',
   })
 
 t.create('no response data')
@@ -45,7 +38,7 @@ t.create('no response data')
       .get('/api/github/phoenixframework/phoenix/dependencies')
       .reply(200)
   )
-  .expectJSON({
-    name: 'dependencies',
-    value: 'invalid',
+  .expectBadge({
+    label: 'dependencies',
+    message: 'invalid',
   })
diff --git a/services/librariesio/librariesio-dependent-repos.tester.js b/services/librariesio/librariesio-dependent-repos.tester.js
index 04fc55fd69ccf6c0203275eed9dd9f47f9fa3a3c..7c8e1badabac3fa3b5cf4da4b4e7bf2652bbb719 100644
--- a/services/librariesio/librariesio-dependent-repos.tester.js
+++ b/services/librariesio/librariesio-dependent-repos.tester.js
@@ -1,22 +1,19 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('dependent repo count')
   .get('/npm/got.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependent repos',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'dependent repos',
+    message: isMetric,
+  })
 
 t.create('dependent repo count (not a package)')
   .get('/npm/foobar-is-not-package.json')
   .timeout(10000)
-  .expectJSON({
-    name: 'dependent repos',
-    value: 'package not found',
+  .expectBadge({
+    label: 'dependent repos',
+    message: 'package not found',
   })
diff --git a/services/librariesio/librariesio-dependents.tester.js b/services/librariesio/librariesio-dependents.tester.js
index de7065ea299322491beda3544af908d1f6f0b454..6e4a038ee02e3dea12e7462d27e08dd6e00f8f9f 100644
--- a/services/librariesio/librariesio-dependents.tester.js
+++ b/services/librariesio/librariesio-dependents.tester.js
@@ -1,29 +1,26 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('dependent count')
   .get('/npm/got.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dependents',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'dependents',
+    message: isMetric,
+  })
 
 t.create('dependent count (nonexistent package)')
   .get('/npm/foobar-is-not-package.json')
   .timeout(10000)
-  .expectJSON({
-    name: 'dependents',
-    value: 'package not found',
+  .expectBadge({
+    label: 'dependents',
+    message: 'package not found',
   })
 
 t.create('dependent count (repo)')
   .get('/github/sindresorhus/got.json')
-  .expectJSON({
-    name: 'dependents',
-    value: 'not supported for repos',
+  .expectBadge({
+    label: 'dependents',
+    message: 'not supported for repos',
   })
diff --git a/services/librariesio/librariesio-sourcerank.tester.js b/services/librariesio/librariesio-sourcerank.tester.js
index 9a819fa17a96c98f888de67dbcbb757731d160dd..9c63dd26bd0b731c6af78ac0b5fbbd4b080842d5 100644
--- a/services/librariesio/librariesio-sourcerank.tester.js
+++ b/services/librariesio/librariesio-sourcerank.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { anyInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('sourcerank')
   .get('/npm/got.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'sourcerank',
-      value: anyInteger,
-    })
-  )
+  .expectBadge({
+    label: 'sourcerank',
+    message: anyInteger,
+  })
 
 t.create('dependent count (not a package)')
   .get('/npm/foobar-is-not-package.json')
-  .expectJSON({
-    name: 'sourcerank',
-    value: 'package not found',
+  .expectBadge({
+    label: 'sourcerank',
+    message: 'package not found',
   })
diff --git a/services/libscore/libscore.tester.js b/services/libscore/libscore.tester.js
index e43e341c0fc6bf20b11a18b415ea8862167a9770..223e8b3d58962844e15770563c2dbdb7c1919b86 100644
--- a/services/libscore/libscore.tester.js
+++ b/services/libscore/libscore.tester.js
@@ -7,7 +7,7 @@ module.exports = t
 
 t.create('no longer available (previously usage statistics)')
   .get('/s/jQuery.json')
-  .expectJSON({
-    name: 'libscore',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'libscore',
+    message: 'no longer available',
   })
diff --git a/services/luarocks/luarocks.tester.js b/services/luarocks/luarocks.tester.js
index aebade77a86eac53b69f61ff46a0e819f3749f51..87074a360e018be93bd04065625afbf7275c4d66 100644
--- a/services/luarocks/luarocks.tester.js
+++ b/services/luarocks/luarocks.tester.js
@@ -9,25 +9,23 @@ const isLuaVersion = Joi.string()
 
 t.create('version')
   .get('/mpeterv/luacheck.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'luarocks',
-      value: isLuaVersion,
-    })
-  )
+  .expectBadge({
+    label: 'luarocks',
+    message: isLuaVersion,
+  })
 
 t.create('specified version')
   .get('/mpeterv/luacheck/0.9.0-1.json')
-  .expectJSON({ name: 'luarocks', value: 'v0.9.0-1' })
+  .expectBadge({ label: 'luarocks', message: 'v0.9.0-1' })
 
 t.create('unknown version')
   .get('/mpeterv/luacheck/0.0.0.json')
-  .expectJSON({ name: 'luarocks', value: 'version not found' })
+  .expectBadge({ label: 'luarocks', message: 'version not found' })
 
 t.create('unknown module')
   .get('/mpeterv/does-not-exist.json')
-  .expectJSON({ name: 'luarocks', value: 'module not found' })
+  .expectBadge({ label: 'luarocks', message: 'module not found' })
 
 t.create('unknown user')
   .get('/nil/does-not-exist.json')
-  .expectJSON({ name: 'luarocks', value: 'user not found' })
+  .expectBadge({ label: 'luarocks', message: 'user not found' })
diff --git a/services/magnumci/magnumci.tester.js b/services/magnumci/magnumci.tester.js
index aba33b639744a154eb68078d2e5bf8ce4f84ad80..d88d56f757622a08a9e714d0e4682d67c9a4f082 100644
--- a/services/magnumci/magnumci.tester.js
+++ b/services/magnumci/magnumci.tester.js
@@ -7,7 +7,7 @@ module.exports = t
 
 t.create('no longer available')
   .get('/ci/96ffb83fa700f069024921b0702e76ff.json')
-  .expectJSON({
-    name: 'magnum ci',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'magnum ci',
+    message: 'no longer available',
   })
diff --git a/services/maintenance/maintenance.tester.js b/services/maintenance/maintenance.tester.js
index 28131acd281a6dd4b9f06cf10240d728146a08c4..030bc10f3fd7713bedd4c31b6a290ec0592087c0 100644
--- a/services/maintenance/maintenance.tester.js
+++ b/services/maintenance/maintenance.tester.js
@@ -6,20 +6,20 @@ const currentYear = new Date().getUTCFullYear()
 
 t.create('yes last maintained 2016 (no)')
   .get('/yes/2016.json')
-  .expectJSON({ name: 'maintained', value: 'no! (as of 2016)' })
+  .expectBadge({ label: 'maintained', message: 'no! (as of 2016)' })
 
 t.create('no longer maintained 2017 (no)')
   .get('/no/2017.json')
-  .expectJSON({ name: 'maintained', value: 'no! (as of 2017)' })
+  .expectBadge({ label: 'maintained', message: 'no! (as of 2017)' })
 
 t.create('yes this year (yes)')
   .get(`/yes/${currentYear}.json`)
-  .expectJSON({ name: 'maintained', value: 'yes' })
+  .expectBadge({ label: 'maintained', message: 'yes' })
 
 t.create(`until end of ${currentYear} (yes)`)
   .get(`/until end of ${currentYear}/${currentYear}.json`)
-  .expectJSON({ name: 'maintained', value: `until end of ${currentYear}` })
+  .expectBadge({ label: 'maintained', message: `until end of ${currentYear}` })
 
 t.create(`stale last maintained ${currentYear - 1} (yes)`)
   .get(`/yes/${currentYear - 1}.json`)
-  .expectJSON({ name: 'maintained', value: `stale (as of ${currentYear})` })
+  .expectBadge({ label: 'maintained', message: `stale (as of ${currentYear})` })
diff --git a/services/matrix/matrix.tester.js b/services/matrix/matrix.tester.js
index 9008e9fa3ba0c8002c66f8521d1076fb852979b5..f24fb6e7c26013ec39dd5f0ad8f6071aa09853d7 100644
--- a/services/matrix/matrix.tester.js
+++ b/services/matrix/matrix.tester.js
@@ -68,9 +68,9 @@ t.create('get room state as guest')
         ])
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: '2 users',
+  .expectBadge({
+    label: 'chat',
+    message: '2 users',
     color: 'brightgreen',
   })
 
@@ -147,18 +147,18 @@ t.create('get room state as member (backup method)')
         ])
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: '2 users',
+  .expectBadge({
+    label: 'chat',
+    message: '2 users',
     color: 'brightgreen',
   })
 
 t.create('bad server or connection')
   .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
   .networkOff()
-  .expectJSON({
-    name: 'chat',
-    value: 'inaccessible',
+  .expectBadge({
+    label: 'chat',
+    message: 'inaccessible',
     color: 'lightgrey',
   })
 
@@ -193,9 +193,9 @@ t.create('non-world readable room')
         })
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: 'room not world readable or is invalid',
+  .expectBadge({
+    label: 'chat',
+    message: 'room not world readable or is invalid',
     color: 'lightgrey',
   })
 
@@ -221,9 +221,9 @@ t.create('invalid token')
         })
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: 'bad auth token',
+  .expectBadge({
+    label: 'chat',
+    message: 'bad auth token',
     color: 'lightgrey',
   })
 
@@ -258,9 +258,9 @@ t.create('unknown request')
         })
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: 'unknown request',
+  .expectBadge({
+    label: 'chat',
+    message: 'unknown request',
     color: 'lightgrey',
   })
 
@@ -286,17 +286,17 @@ t.create('unknown alias')
         })
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: 'room not found',
+  .expectBadge({
+    label: 'chat',
+    message: 'room not found',
     color: 'red',
   })
 
 t.create('invalid alias')
   .get('/ALIASDUMMY.dumb.json?style=_shields_test')
-  .expectJSON({
-    name: 'chat',
-    value: 'invalid alias',
+  .expectBadge({
+    label: 'chat',
+    message: 'invalid alias',
     color: 'red',
   })
 
@@ -365,9 +365,9 @@ t.create('server uses a custom port')
         ])
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: '2 users',
+  .expectBadge({
+    label: 'chat',
+    message: '2 users',
     color: 'brightgreen',
   })
 
@@ -438,19 +438,17 @@ t.create('specify the homeserver fqdn')
         ])
       )
   )
-  .expectJSON({
-    name: 'chat',
-    value: '2 users',
+  .expectBadge({
+    label: 'chat',
+    message: '2 users',
     color: 'brightgreen',
   })
 
 t.create('test on real matrix room for API compliance')
   .get('/twim:matrix.org.json?style=_shields_test')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'chat',
-      value: Joi.string().regex(/^[0-9]+ users$/),
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'chat',
+    message: Joi.string().regex(/^[0-9]+ users$/),
+    color: 'brightgreen',
+  })
diff --git a/services/maven-central/maven-central.tester.js b/services/maven-central/maven-central.tester.js
index 58b45027871b01034b5db0d80ba546815135dcda..c7c64c8086100085fe242f7362c4157c9bab0021 100644
--- a/services/maven-central/maven-central.tester.js
+++ b/services/maven-central/maven-central.tester.js
@@ -8,30 +8,26 @@ module.exports = t
 
 t.create('latest version')
   .get('/v/com.github.fabriziocucci/yacl4j.json') // http://repo1.maven.org/maven2/com/github/fabriziocucci/yacl4j/
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'maven-central',
-      value: Joi.string().regex(/^v(.*)$/),
-    })
-  )
+  .expectBadge({
+    label: 'maven-central',
+    message: Joi.string().regex(/^v(.*)$/),
+  })
 
 t.create('latest 0.8 version')
   .get('/v/com.github.fabriziocucci/yacl4j/0.8.json') // http://repo1.maven.org/maven2/com/github/fabriziocucci/yacl4j/
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'maven-central',
-      value: Joi.string().regex(/^v0\.8(.*)$/),
-    })
-  )
+  .expectBadge({
+    label: 'maven-central',
+    message: Joi.string().regex(/^v0\.8(.*)$/),
+  })
 
 t.create('inexistent artifact')
   .get('/v/inexistent-group-id/inexistent-artifact-id.json')
-  .expectJSON({ name: 'maven-central', value: 'invalid' })
+  .expectBadge({ label: 'maven-central', message: 'invalid' })
 
 t.create('connection error')
   .get('/v/com.github.fabriziocucci/yacl4j.json')
   .networkOff()
-  .expectJSON({ name: 'maven-central', value: 'inaccessible' })
+  .expectBadge({ label: 'maven-central', message: 'inaccessible' })
 
 t.create('xml parsing error')
   .get('/v/com.github.fabriziocucci/yacl4j.json')
@@ -40,4 +36,4 @@ t.create('xml parsing error')
       .get('/com/github/fabriziocucci/yacl4j/maven-metadata.xml')
       .reply(200, 'this should be a valid xml')
   )
-  .expectJSON({ name: 'maven-central', value: 'invalid' })
+  .expectBadge({ label: 'maven-central', message: 'invalid' })
diff --git a/services/maven-metadata/maven-metadata.tester.js b/services/maven-metadata/maven-metadata.tester.js
index f3a5a00f498831fa61524d59b0997be8955e9902..51eeeeb307064e10420ea773def4356416b23e51 100644
--- a/services/maven-metadata/maven-metadata.tester.js
+++ b/services/maven-metadata/maven-metadata.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 
@@ -13,15 +12,13 @@ t.create('valid maven-metadata.xml uri')
   .get(
     '/v/http/central.maven.org/maven2/com/google/code/gson/gson/maven-metadata.xml.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'maven',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'maven',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('invalid maven-metadata.xml uri')
   .get(
     '/v/http/central.maven.org/maven2/com/google/code/gson/gson/foobar.xml.json'
   )
-  .expectJSON({ name: 'maven', value: 'not found' })
+  .expectBadge({ label: 'maven', message: 'not found' })
diff --git a/services/microbadger/microbadger.tester.js b/services/microbadger/microbadger.tester.js
index a07a37eeaf47f704f9cf71b3291612ba48a2bd3b..40a79c411d7b1787b6f9515db454b96c70804ad4 100644
--- a/services/microbadger/microbadger.tester.js
+++ b/services/microbadger/microbadger.tester.js
@@ -10,47 +10,39 @@ module.exports = t
 
 t.create('image size without a specified tag')
   .get('/image-size/fedora/apache.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'image size',
-      value: isFileSize,
-    })
-  )
+  .expectBadge({
+    label: 'image size',
+    message: isFileSize,
+  })
 
 t.create('image size with a specified tag')
   .get('/image-size/fedora/apache/latest.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'image size',
-      value: isFileSize,
-    })
-  )
+  .expectBadge({
+    label: 'image size',
+    message: isFileSize,
+  })
 
 t.create('layers without a specified tag')
   .get('/layers/_/alpine.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'layers',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'layers',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('layers with a specified tag')
   .get('/layers/_/alpine/2.7.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'layers',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'layers',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('specified tag when repository has only one')
   .get('/layers/_/alpine/wrong-tag.json')
-  .expectJSON({ name: 'layers', value: 'not found' })
+  .expectBadge({ label: 'layers', message: 'not found' })
 
 t.create('nonexistent repository')
   .get('/layers/_/unknown.json')
@@ -59,7 +51,7 @@ t.create('nonexistent repository')
       .get('/v1/images/library/unknown')
       .reply(404)
   )
-  .expectJSON({ name: 'layers', value: 'not found' })
+  .expectBadge({ label: 'layers', message: 'not found' })
 
 t.create('nonexistent tag')
   .get('/layers/_/unknown/wrong-tag.json')
@@ -68,7 +60,7 @@ t.create('nonexistent tag')
       .get('/v1/images/library/unknown')
       .reply(200, { Versions: [] })
   )
-  .expectJSON({ name: 'layers', value: 'not found' })
+  .expectBadge({ label: 'layers', message: 'not found' })
 
 t.create('server error')
   .get('/image-size/_/hello-world.json')
@@ -77,12 +69,12 @@ t.create('server error')
       .get('/v1/images/library/hello-world')
       .reply(500, 'Something went wrong')
   )
-  .expectJSON({ name: 'image size', value: 'inaccessible' })
+  .expectBadge({ label: 'image size', message: 'inaccessible' })
 
 t.create('connection error')
   .get('/image-size/_/hello-world.json')
   .networkOff()
-  .expectJSON({ name: 'image size', value: 'inaccessible' })
+  .expectBadge({ label: 'image size', message: 'inaccessible' })
 
 t.create('unexpected response')
   .get('/image-size/_/hello-world.json')
@@ -91,7 +83,7 @@ t.create('unexpected response')
       .get('/v1/images/library/hello-world')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'image size', value: 'error' })
+  .expectBadge({ label: 'image size', message: 'error' })
 
 t.create('missing download size')
   .get('/image-size/puppet/puppetserver.json')
@@ -100,4 +92,4 @@ t.create('missing download size')
       .get('/v1/images/puppet/puppetserver')
       .reply(200, {})
   )
-  .expectJSON({ name: 'image size', value: 'unknown' })
+  .expectBadge({ label: 'image size', message: 'unknown' })
diff --git a/services/mozilla-observatory/mozilla-observatory.tester.js b/services/mozilla-observatory/mozilla-observatory.tester.js
index db9f59ff13e835e403544da2afddab41e9a49c4f..00899985b119cafcbc13f4753ef528f62875c473 100644
--- a/services/mozilla-observatory/mozilla-observatory.tester.js
+++ b/services/mozilla-observatory/mozilla-observatory.tester.js
@@ -7,27 +7,23 @@ const validColors = ['brightgreen', 'green', 'yellow', 'orange', 'red']
 
 t.create('request on observatory.mozilla.org')
   .get('/grade-score/observatory.mozilla.org.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'observatory',
-      value: Joi.string().regex(/^[ABCDEF][+-]? \([0-9]{1,3}\/100\)$/),
-      color: Joi.string()
-        .valid(validColors)
-        .required(),
-    })
-  )
+  .expectBadge({
+    label: 'observatory',
+    message: Joi.string().regex(/^[ABCDEF][+-]? \([0-9]{1,3}\/100\)$/),
+    color: Joi.string()
+      .valid(validColors)
+      .required(),
+  })
 
 t.create('request on observatory.mozilla.org with inclusion in public results')
   .get('/grade-score/observatory.mozilla.org.json?publish&style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'observatory',
-      value: Joi.string().regex(/^[ABCDEF][+-]? \([0-9]{1,3}\/100\)$/),
-      color: Joi.string()
-        .valid(validColors)
-        .required(),
-    })
-  )
+  .expectBadge({
+    label: 'observatory',
+    message: Joi.string().regex(/^[ABCDEF][+-]? \([0-9]{1,3}\/100\)$/),
+    color: Joi.string()
+      .valid(validColors)
+      .required(),
+  })
 
 t.create('grade without score (mock)')
   .get('/grade/foo.bar.json?style=_shields_test')
@@ -36,9 +32,9 @@ t.create('grade without score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'A', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'A',
+  .expectBadge({
+    label: 'observatory',
+    message: 'A',
     color: 'brightgreen',
   })
 
@@ -49,9 +45,9 @@ t.create('grade A with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'A', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'A (115/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'A (115/100)',
     color: 'brightgreen',
   })
 
@@ -62,9 +58,9 @@ t.create('grade A+ with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'A+', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'A+ (115/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'A+ (115/100)',
     color: 'brightgreen',
   })
 
@@ -75,9 +71,9 @@ t.create('grade A- with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'A-', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'A- (115/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'A- (115/100)',
     color: 'brightgreen',
   })
 
@@ -88,9 +84,9 @@ t.create('grade B with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'B', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'B (115/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'B (115/100)',
     color: 'green',
   })
 
@@ -101,9 +97,9 @@ t.create('grade B+ with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'B+', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'B+ (115/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'B+ (115/100)',
     color: 'green',
   })
 
@@ -114,9 +110,9 @@ t.create('grade B- with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'B-', score: 115 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'B- (115/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'B- (115/100)',
     color: 'green',
   })
 
@@ -127,9 +123,9 @@ t.create('grade C with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'C', score: 80 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'C (80/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'C (80/100)',
     color: 'yellow',
   })
 
@@ -140,9 +136,9 @@ t.create('grade C+ with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'C+', score: 80 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'C+ (80/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'C+ (80/100)',
     color: 'yellow',
   })
 
@@ -153,9 +149,9 @@ t.create('grade C- with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'C-', score: 80 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'C- (80/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'C- (80/100)',
     color: 'yellow',
   })
 
@@ -166,9 +162,9 @@ t.create('grade D with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'D', score: 15 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'D (15/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'D (15/100)',
     color: 'orange',
   })
 
@@ -179,9 +175,9 @@ t.create('grade D+ with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'D+', score: 15 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'D+ (15/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'D+ (15/100)',
     color: 'orange',
   })
 
@@ -192,9 +188,9 @@ t.create('grade D- with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'D-', score: 15 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'D- (15/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'D- (15/100)',
     color: 'orange',
   })
 
@@ -205,9 +201,9 @@ t.create('grade E with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'E', score: 15 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'E (15/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'E (15/100)',
     color: 'orange',
   })
 
@@ -218,9 +214,9 @@ t.create('grade E+ with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'E+', score: 15 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'E+ (15/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'E+ (15/100)',
     color: 'orange',
   })
 
@@ -231,9 +227,9 @@ t.create('grade E- with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'E-', score: 15 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'E- (15/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'E- (15/100)',
     color: 'orange',
   })
 
@@ -244,9 +240,9 @@ t.create('grade F with score (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FINISHED', grade: 'F', score: 0 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'F (0/100)',
+  .expectBadge({
+    label: 'observatory',
+    message: 'F (0/100)',
     color: 'red',
   })
 
@@ -257,9 +253,9 @@ t.create('aborted (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'ABORTED', grade: null, score: null })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'aborted',
+  .expectBadge({
+    label: 'observatory',
+    message: 'aborted',
     color: 'lightgrey',
   })
 
@@ -270,9 +266,9 @@ t.create('failed (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'FAILED', grade: null, score: null })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'failed',
+  .expectBadge({
+    label: 'observatory',
+    message: 'failed',
     color: 'lightgrey',
   })
 
@@ -283,9 +279,9 @@ t.create('pending (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'PENDING', grade: null, score: null })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'pending',
+  .expectBadge({
+    label: 'observatory',
+    message: 'pending',
     color: 'lightgrey',
   })
 
@@ -296,9 +292,9 @@ t.create('starting (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'STARTING', grade: null, score: null })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'starting',
+  .expectBadge({
+    label: 'observatory',
+    message: 'starting',
     color: 'lightgrey',
   })
 
@@ -309,9 +305,9 @@ t.create('running (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'RUNNING', grade: null, score: null })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'running',
+  .expectBadge({
+    label: 'observatory',
+    message: 'running',
     color: 'lightgrey',
   })
 
@@ -322,8 +318,8 @@ t.create('invalid response with grade and score but not finished (mock)')
       .post('/api/v1/analyze?host=foo.bar')
       .reply(200, { state: 'RUNNING', grade: 'A+', score: 135 })
   )
-  .expectJSON({
-    name: 'observatory',
-    value: 'invalid response data',
+  .expectBadge({
+    label: 'observatory',
+    message: 'invalid response data',
     color: 'lightgrey',
   })
diff --git a/services/myget/myget.tester.js b/services/myget/myget.tester.js
index ae88fc7be0b83fbdc6c27d04f095fc94d9b8b893..e9fa697263a2074fc9e39cef81128292cd237b34 100644
--- a/services/myget/myget.tester.js
+++ b/services/myget/myget.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const {
   isMetric,
@@ -24,33 +23,29 @@ const t = (module.exports = new ServiceTester({
 
 t.create('total downloads (valid)')
   .get('/myget/mongodb/dt/MongoDB.Driver.Core.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (tenant)')
   .get('/dotnet.myget/dotnet-coreclr/dt/Microsoft.DotNet.CoreCLR.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (not found)')
   .get('/myget/mongodb/dt/not-a-real-package.json')
-  .expectJSON({ name: 'downloads', value: 'package not found' })
+  .expectBadge({ label: 'downloads', message: 'package not found' })
 
 // This tests the erroring behavior in regular-update.
 t.create('total downloads (connection error)')
   .get('/myget/mongodb/dt/MongoDB.Driver.Core.json')
   .networkOff()
-  .expectJSON({
-    name: 'downloads',
-    value: 'intermediate resource inaccessible',
+  .expectBadge({
+    label: 'downloads',
+    message: 'intermediate resource inaccessible',
   })
 
 // This tests the erroring behavior in regular-update.
@@ -61,30 +56,26 @@ t.create('total downloads (unexpected first response)')
       .get('/F/mongodb/api/v3/index.json')
       .reply(invalidJSON)
   )
-  .expectJSON({
-    name: 'downloads',
-    value: 'unparseable intermediate json response',
+  .expectBadge({
+    label: 'downloads',
+    message: 'unparseable intermediate json response',
   })
 
 // version
 
 t.create('version (valid)')
   .get('/myget/mongodb/v/MongoDB.Driver.Core.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'mongodb',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'mongodb',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('total downloads (tenant)')
   .get('/dotnet.myget/dotnet-coreclr/v/Microsoft.DotNet.CoreCLR.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'dotnet-coreclr',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'dotnet-coreclr',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (mocked, yellow badge)')
   .get('/myget/mongodb/v/MongoDB.Driver.Core.json?style=_shields_test')
@@ -100,9 +91,9 @@ t.create('version (mocked, yellow badge)')
       )
       .reply(200, nuGetV3VersionJsonWithDash)
   )
-  .expectJSON({
-    name: 'mongodb',
-    value: 'v1.2-beta',
+  .expectBadge({
+    label: 'mongodb',
+    message: 'v1.2-beta',
     color: 'yellow',
   })
 
@@ -120,9 +111,9 @@ t.create('version (mocked, orange badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharZero)
   )
-  .expectJSON({
-    name: 'mongodb',
-    value: 'v0.35',
+  .expectBadge({
+    label: 'mongodb',
+    message: 'v0.35',
     color: 'orange',
   })
 
@@ -140,26 +131,24 @@ t.create('version (mocked, blue badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharNotZero)
   )
-  .expectJSON({
-    name: 'mongodb',
-    value: 'v1.2.7',
+  .expectBadge({
+    label: 'mongodb',
+    message: 'v1.2.7',
     color: 'blue',
   })
 
 t.create('version (not found)')
   .get('/myget/foo/v/not-a-real-package.json')
-  .expectJSON({ name: 'myget', value: 'package not found' })
+  .expectBadge({ label: 'myget', message: 'package not found' })
 
 // version (pre)
 
 t.create('version (pre) (valid)')
   .get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'mongodb',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'mongodb',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (pre) (mocked, yellow badge)')
   .get('/myget/mongodb/vpre/MongoDB.Driver.Core.json?style=_shields_test')
@@ -175,9 +164,9 @@ t.create('version (pre) (mocked, yellow badge)')
       )
       .reply(200, nuGetV3VersionJsonWithDash)
   )
-  .expectJSON({
-    name: 'mongodb',
-    value: 'v1.2-beta',
+  .expectBadge({
+    label: 'mongodb',
+    message: 'v1.2-beta',
     color: 'yellow',
   })
 
@@ -195,9 +184,9 @@ t.create('version (pre) (mocked, orange badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharZero)
   )
-  .expectJSON({
-    name: 'mongodb',
-    value: 'v0.35',
+  .expectBadge({
+    label: 'mongodb',
+    message: 'v0.35',
     color: 'orange',
   })
 
@@ -215,12 +204,12 @@ t.create('version (pre) (mocked, blue badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharNotZero)
   )
-  .expectJSON({
-    name: 'mongodb',
-    value: 'v1.2.7',
+  .expectBadge({
+    label: 'mongodb',
+    message: 'v1.2.7',
     color: 'blue',
   })
 
 t.create('version (pre) (not found)')
   .get('/myget/foo/vpre/not-a-real-package.json')
-  .expectJSON({ name: 'myget', value: 'package not found' })
+  .expectBadge({ label: 'myget', message: 'package not found' })
diff --git a/services/nexus/nexus.tester.js b/services/nexus/nexus.tester.js
index b345ee15f18a4bf7a6eeb2fd18a7ad362f7b9a06..c26e3ada2f070936671a29bde85cd18c53724ea9 100644
--- a/services/nexus/nexus.tester.js
+++ b/services/nexus/nexus.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const sinon = require('sinon')
 const {
   isVPlusDottedVersionNClausesWithOptionalSuffix: isVersion,
@@ -20,70 +19,62 @@ function mockNexusCreds() {
 
 t.create('live: search release version valid artifact')
   .get('/r/https/repository.jboss.org/nexus/jboss/jboss-client.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nexus',
-      value: isVersion,
-    })
-  )
+  .expectBadge({
+    label: 'nexus',
+    message: isVersion,
+  })
 
 t.create('live: search release version of an inexistent artifact')
   .get('/r/https/repository.jboss.org/nexus/jboss/inexistent-artifact-id.json')
-  .expectJSON({
-    name: 'nexus',
-    value: 'artifact or version not found',
+  .expectBadge({
+    label: 'nexus',
+    message: 'artifact or version not found',
   })
 
 t.create('live: search snapshot version valid snapshot artifact')
   .get('/s/https/repository.jboss.org/nexus/com.progress.fuse/fusehq.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nexus',
-      value: isVersion,
-    })
-  )
+  .expectBadge({
+    label: 'nexus',
+    message: isVersion,
+  })
 
 t.create('live: search snapshot version of a release artifact')
   .get('/s/https/repository.jboss.org/nexus/jboss/jboss-client.json')
-  .expectJSON({ name: 'nexus', value: 'no snapshot versions found' })
+  .expectBadge({ label: 'nexus', message: 'no snapshot versions found' })
 
 t.create('live: search snapshot version of an inexistent artifact')
   .get(
     '/s/https/repository.jboss.org/nexus/jboss/inexistent-artifact-id.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'artifact or version not found',
+  .expectBadge({
+    label: 'nexus',
+    message: 'artifact or version not found',
     color: 'red',
   })
 
 t.create('live: repository version')
   .get('/developer/https/repository.jboss.org/nexus/ai.h2o/h2o-automl.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nexus',
-      value: isVersion,
-    })
-  )
+  .expectBadge({
+    label: 'nexus',
+    message: isVersion,
+  })
 
 t.create('live: repository version with query')
   .get(
     '/fs-public-snapshots/https/repository.jboss.org/nexus/com.progress.fuse/fusehq:c=agent-apple-osx:p=tar.gz.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nexus',
-      value: isVersion,
-    })
-  )
+  .expectBadge({
+    label: 'nexus',
+    message: isVersion,
+  })
 
 t.create('live: repository version of an inexistent artifact')
   .get(
     '/developer/https/repository.jboss.org/nexus/jboss/inexistent-artifact-id.json'
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'artifact not found',
+  .expectBadge({
+    label: 'nexus',
+    message: 'artifact not found',
   })
 
 t.create('snapshot version with + in version')
@@ -96,13 +87,11 @@ t.create('snapshot version with + in version')
       .query({ g: 'com.progress.fuse', a: 'fusehq' })
       .reply(200, { data: [{ version: '7.0.1+19-8844c122-SNAPSHOT' }] })
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nexus',
-      color: 'orange',
-      value: isVersion,
-    })
-  )
+  .expectBadge({
+    label: 'nexus',
+    color: 'orange',
+    message: isVersion,
+  })
 
 t.create('search snapshot version not in latestSnapshot')
   .get(
@@ -114,9 +103,9 @@ t.create('search snapshot version not in latestSnapshot')
       .query({ g: 'com.progress.fuse', a: 'fusehq' })
       .reply(200, { data: [{ version: '7.0.1-SNAPSHOT' }] })
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'v7.0.1-SNAPSHOT',
+  .expectBadge({
+    label: 'nexus',
+    message: 'v7.0.1-SNAPSHOT',
     color: 'orange',
   })
 
@@ -130,9 +119,9 @@ t.create('search snapshot no snapshot versions')
       .query({ g: 'com.progress.fuse', a: 'fusehq' })
       .reply(200, { data: [{ version: '1.2.3' }] })
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'no snapshot versions found',
+  .expectBadge({
+    label: 'nexus',
+    message: 'no snapshot versions found',
     color: 'lightgrey',
   })
 
@@ -146,9 +135,9 @@ t.create('search release version')
       .query({ g: 'jboss', a: 'jboss-client' })
       .reply(200, { data: [{ latestRelease: '1.0.0' }] })
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'v1.0.0',
+  .expectBadge({
+    label: 'nexus',
+    message: 'v1.0.0',
     color: 'blue',
   })
 
@@ -172,9 +161,9 @@ t.create('repository release version')
         },
       })
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'v1.2.3',
+  .expectBadge({
+    label: 'nexus',
+    message: 'v1.2.3',
     color: 'blue',
   })
 
@@ -197,9 +186,9 @@ t.create('repository release version')
         },
       })
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'v1.0.0',
+  .expectBadge({
+    label: 'nexus',
+    message: 'v1.0.0',
     color: 'blue',
   })
 
@@ -224,9 +213,9 @@ t.create('user query params')
         },
       })
   )
-  .expectJSON({
-    name: 'nexus',
-    value: 'v3.2.1',
+  .expectBadge({
+    label: 'nexus',
+    message: 'v3.2.1',
     color: 'blue',
   })
 
@@ -248,8 +237,8 @@ t.create('auth')
       .reply(200, { data: [{ latestRelease: '2.3.4' }] })
   )
   .finally(sinon.restore)
-  .expectJSON({
-    name: 'nexus',
-    value: 'v2.3.4',
+  .expectBadge({
+    label: 'nexus',
+    message: 'v2.3.4',
     color: 'blue',
   })
diff --git a/services/node/node.tester.js b/services/node/node.tester.js
index a7a5ebeb8960667786d760dc2303cacd88903d31..ee45643be4269b13087462da7df444afae94de00 100644
--- a/services/node/node.tester.js
+++ b/services/node/node.tester.js
@@ -11,39 +11,39 @@ function expectSemverRange(value) {
 
 t.create('gets the node version of passport')
   .get('/passport.json')
-  .expectJSONTypes(Joi.object({ name: 'node' }).unknown())
+  .expectBadge({ label: 'node', message: Joi.string() })
   .afterJSON(json => {
     expectSemverRange(json.value)
   })
 
 t.create('gets the node version of @stdlib/stdlib')
   .get('/@stdlib/stdlib.json')
-  .expectJSONTypes(Joi.object({ name: 'node' }).unknown())
+  .expectBadge({ label: 'node', message: Joi.string() })
   .afterJSON(json => {
     expectSemverRange(json.value)
   })
 
 t.create("gets the tagged release's node version version of ionic")
   .get('/ionic/next.json')
-  .expectJSONTypes(Joi.object({ name: 'node@next' }).unknown())
+  .expectBadge({ label: 'node@next', message: Joi.string() })
   .afterJSON(json => {
     expectSemverRange(json.value)
   })
 
 t.create('gets the node version of passport from a custom registry')
   .get('/passport.json?registry_uri=https://registry.npmjs.com')
-  .expectJSONTypes(Joi.object({ name: 'node' }).unknown())
+  .expectBadge({ label: 'node', message: Joi.string() })
   .afterJSON(json => {
     expectSemverRange(json.value)
   })
 
 t.create("gets the tagged release's node version of @cycle/core")
   .get('/@cycle/core/canary.json')
-  .expectJSONTypes(Joi.object({ name: 'node@canary' }).unknown())
+  .expectBadge({ label: 'node@canary', message: Joi.string() })
   .afterJSON(json => {
     expectSemverRange(json.value)
   })
 
 t.create('invalid package name')
   .get('/frodo-is-not-a-package.json')
-  .expectJSON({ name: 'node', value: 'package not found' })
+  .expectBadge({ label: 'node', message: 'package not found' })
diff --git a/services/nodeping/nodeping-status.tester.js b/services/nodeping/nodeping-status.tester.js
index 25a3a31b8a74494aaf148747bb044d0ac20e0923..f10eb170c519583e6b6ea918ec2cd43852ed9eb4 100644
--- a/services/nodeping/nodeping-status.tester.js
+++ b/services/nodeping/nodeping-status.tester.js
@@ -3,13 +3,12 @@
 const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 
-t.create('NodePing status - live').get(
-  '/jkiwn052-ntpp-4lbb-8d45-ihew6d9ucoei.json'
-)
-Joi.object().keys({
-  name: 'Status',
-  value: Joi.equal('up', 'down').required(),
-})
+t.create('NodePing status - live')
+  .get('/jkiwn052-ntpp-4lbb-8d45-ihew6d9ucoei.json')
+  .expectBadge({
+    label: 'Status',
+    message: Joi.equal('up', 'down').required(),
+  })
 
 t.create('NodePing status - up (mock)')
   .get('/jkiwn052-ntpp-4lbb-8d45-ihew6d9ucoei.json')
@@ -20,9 +19,9 @@ t.create('NodePing status - up (mock)')
       )
       .reply(200, [{ su: true }])
   )
-  .expectJSON({
-    name: 'Status',
-    value: 'up',
+  .expectBadge({
+    label: 'Status',
+    message: 'up',
   })
 
 t.create('NodePing status - down (mock)')
@@ -34,9 +33,9 @@ t.create('NodePing status - down (mock)')
       )
       .reply(200, [{ su: false }])
   )
-  .expectJSON({
-    name: 'Status',
-    value: 'down',
+  .expectBadge({
+    label: 'Status',
+    message: 'down',
   })
 
 t.create('NodePing status - custom up color/message')
@@ -50,9 +49,9 @@ t.create('NodePing status - custom up color/message')
       )
       .reply(200, [{ su: true }])
   )
-  .expectJSON({
-    name: 'Status',
-    value: 'happy',
+  .expectBadge({
+    label: 'Status',
+    message: 'happy',
     color: 'blue',
   })
 
@@ -67,8 +66,8 @@ t.create('NodePing status - custom down color/message')
       )
       .reply(200, [{ su: false }])
   )
-  .expectJSON({
-    name: 'Status',
-    value: 'sad',
+  .expectBadge({
+    label: 'Status',
+    message: 'sad',
     color: 'yellow',
   })
diff --git a/services/nodeping/nodeping-uptime.tester.js b/services/nodeping/nodeping-uptime.tester.js
index 7ba3021bd38c407831eec9b0bb7d360727193a8b..cd034ff1ee181e3070eb8bb61e0e06e437542708 100644
--- a/services/nodeping/nodeping-uptime.tester.js
+++ b/services/nodeping/nodeping-uptime.tester.js
@@ -5,9 +5,9 @@ const { isPercentage } = require('../test-validators')
 
 t.create('NodePing uptime - live')
   .get('/jkiwn052-ntpp-4lbb-8d45-ihew6d9ucoei.json')
-  .expectJSONTypes({
-    name: 'Uptime',
-    value: isPercentage,
+  .expectBadge({
+    label: 'Uptime',
+    message: isPercentage,
   })
 
 t.create('NodePing uptime - 100% (mock)')
@@ -23,9 +23,9 @@ t.create('NodePing uptime - 100% (mock)')
       )
       .reply(200, [{ uptime: 100 }])
   )
-  .expectJSON({
-    name: 'Uptime',
-    value: '100%',
+  .expectBadge({
+    label: 'Uptime',
+    message: '100%',
     color: 'brightgreen',
   })
 
@@ -42,9 +42,9 @@ t.create('NodePing uptime - 99.999% (mock)')
       )
       .reply(200, [{ uptime: 99.999 }])
   )
-  .expectJSON({
-    name: 'Uptime',
-    value: '99.999%',
+  .expectBadge({
+    label: 'Uptime',
+    message: '99.999%',
     color: 'green',
   })
 
@@ -61,9 +61,9 @@ t.create('NodePing uptime - 99.001% (mock)')
       )
       .reply(200, [{ uptime: 99.001 }])
   )
-  .expectJSON({
-    name: 'Uptime',
-    value: '99.001%',
+  .expectBadge({
+    label: 'Uptime',
+    message: '99.001%',
     color: 'yellow',
   })
 
@@ -80,8 +80,8 @@ t.create('NodePing uptime - 90.001% (mock)')
       )
       .reply(200, [{ uptime: 90.001 }])
   )
-  .expectJSON({
-    name: 'Uptime',
-    value: '90.001%',
+  .expectBadge({
+    label: 'Uptime',
+    message: '90.001%',
     color: 'red',
   })
diff --git a/services/npm/npm-collaborators.tester.js b/services/npm/npm-collaborators.tester.js
index 3c4c4cbc0ce8f0ebb2a9a9c5d1a3edceeea320f8..45f6c9bab680a7bdfb66e32ef75ce24621050a2b 100644
--- a/services/npm/npm-collaborators.tester.js
+++ b/services/npm/npm-collaborators.tester.js
@@ -1,24 +1,19 @@
 'use strict'
 
-const Joi = require('joi')
 const { nonNegativeInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('gets the contributor count')
   .get('/prettier.json')
-  .expectJSONTypes(
-    Joi.object({ name: 'npm collaborators', value: nonNegativeInteger })
-  )
+  .expectBadge({ label: 'npm collaborators', message: nonNegativeInteger })
 
 t.create('gets the contributor count from a custom registry')
   .get('/prettier.json?registry_uri=https://registry.npmjs.com')
-  .expectJSONTypes(
-    Joi.object({ name: 'npm collaborators', value: nonNegativeInteger })
-  )
+  .expectBadge({ label: 'npm collaborators', message: nonNegativeInteger })
 
 t.create('contributor count for unknown package')
   .get('/npm-registry-does-not-have-this-package.json')
-  .expectJSON({
-    name: 'npm collaborators',
-    value: 'package not found',
+  .expectBadge({
+    label: 'npm collaborators',
+    message: 'package not found',
   })
diff --git a/services/npm/npm-dependency-version.tester.js b/services/npm/npm-dependency-version.tester.js
index 61ed87a6dd314c90c936e3d090d164856bbae619..a835f1357739f9dcad19630f0467396e66c3c895 100644
--- a/services/npm/npm-dependency-version.tester.js
+++ b/services/npm/npm-dependency-version.tester.js
@@ -1,48 +1,39 @@
 'use strict'
 
-const Joi = require('joi')
 const { semverRange } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('gets the peer dependency version')
   .get('/react-boxplot/peer/react.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'react',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'react',
+    message: semverRange,
+  })
 
 t.create('gets the dev dependency version')
   .get('/react-boxplot/dev/react.json?label=react%20tested')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'react tested',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'react tested',
+    message: semverRange,
+  })
 
 t.create('gets the dev dependency version (scoped)')
   .get('/@metabolize/react-flexbox-svg/dev/eslint.json?')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'eslint',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'eslint',
+    message: semverRange,
+  })
 
 t.create('gets the prod dependency version')
   .get('/react-boxplot/simple-statistics.json')
-  .expectJSONTypes(
-    Joi.object({
-      name: 'simple-statistics',
-      value: semverRange,
-    })
-  )
+  .expectBadge({
+    label: 'simple-statistics',
+    message: semverRange,
+  })
 
 t.create('unknown dependency')
   .get('/react-boxplot/dev/i-made-this-up.json')
-  .expectJSON({
-    name: 'dependency',
-    value: 'dev dependency not found',
+  .expectBadge({
+    label: 'dependency',
+    message: 'dev dependency not found',
   })
diff --git a/services/npm/npm-downloads.tester.js b/services/npm/npm-downloads.tester.js
index 0d2b27858c33b79303b1d336b289e428e625a72e..428e6b0ad4b057b4e54b28346392690d58e71133 100644
--- a/services/npm/npm-downloads.tester.js
+++ b/services/npm/npm-downloads.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric } = require('../test-validators')
 
@@ -13,17 +12,15 @@ module.exports = t
 
 t.create('total downloads of left-pad')
   .get('/dt/left-pad.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+    color: 'brightgreen',
+  })
 
 t.create('total downloads of @cycle/core')
   .get('/dt/@cycle/core.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('total downloads of package with zero downloads')
   .get('/dt/package-no-downloads.json?style=_shields_test')
@@ -34,7 +31,7 @@ t.create('total downloads of package with zero downloads')
         downloads: [{ downloads: 0, day: '2018-01-01' }],
       })
   )
-  .expectJSON({ name: 'downloads', value: '0', color: 'red' })
+  .expectBadge({ label: 'downloads', message: '0', color: 'red' })
 
 t.create('exact total downloads value')
   .get('/dt/exact-value.json')
@@ -48,12 +45,12 @@ t.create('exact total downloads value')
         ],
       })
   )
-  .expectJSON({ name: 'downloads', value: '5' })
+  .expectBadge({ label: 'downloads', message: '5' })
 
 t.create('total downloads of unknown package')
   .get('/dt/npm-api-does-not-have-this-package.json?style=_shields_test')
-  .expectJSON({
-    name: 'downloads',
-    value: 'package not found or too new',
+  .expectBadge({
+    label: 'downloads',
+    message: 'package not found or too new',
     color: 'red',
   })
diff --git a/services/npm/npm-license.tester.js b/services/npm/npm-license.tester.js
index e714c2d010f31ad95c714509be05b59f4eddd980..a9c74a7580e88867843f9cb2082c3a41107d919d 100644
--- a/services/npm/npm-license.tester.js
+++ b/services/npm/npm-license.tester.js
@@ -1,39 +1,38 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('gets the license of express')
   .get('/express.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'license', value: 'MIT' }))
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 t.create('gets the license of express from a custom registry')
   .get('/express.json?registry_uri=https://registry.npmjs.com')
-  .expectJSONTypes(Joi.object().keys({ name: 'license', value: 'MIT' }))
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 t.create('public domain license')
   .get('/redux-auth.json?style=_shields_test')
-  .expectJSON({ name: 'license', value: 'WTFPL', color: '#7cd958' })
+  .expectBadge({ label: 'license', message: 'WTFPL', color: '#7cd958' })
 
 t.create('copyleft license')
   .get('/trianglify.json?style=_shields_test')
-  .expectJSON({ name: 'license', value: 'GPL-3.0', color: 'orange' })
+  .expectBadge({ label: 'license', message: 'GPL-3.0', color: 'orange' })
 
 t.create('permissive license')
   .get('/express.json?style=_shields_test')
-  .expectJSON({ name: 'license', value: 'MIT', color: 'green' })
+  .expectBadge({ label: 'license', message: 'MIT', color: 'green' })
 
 t.create('permissive license for scoped package')
   .get('/@cycle%2Fcore.json?style=_shields_test')
-  .expectJSON({ name: 'license', value: 'MIT', color: 'green' })
+  .expectBadge({ label: 'license', message: 'MIT', color: 'green' })
 
 t.create(
   'permissive and copyleft licenses (SPDX license expression syntax version 2.0)'
 )
   .get('/rho-cc-promise.json?style=_shields_test')
-  .expectJSON({
-    name: 'license',
-    value: '(MPL-2.0 OR MIT)',
+  .expectBadge({
+    label: 'license',
+    message: '(MPL-2.0 OR MIT)',
     color: 'lightgrey',
   })
 
@@ -43,11 +42,11 @@ t.create('license for package without a license property')
     nock('https://registry.npmjs.org')
       .get('/package-without-license/latest')
       .reply(200, {
-        name: 'package-without-license',
+        label: 'package-without-license',
         maintainers: [],
       })
   )
-  .expectJSON({ name: 'license', value: 'missing', color: 'red' })
+  .expectBadge({ label: 'license', message: 'missing', color: 'red' })
 
 t.create('license for package with a license object')
   .get('/package-license-object.json?style=_shields_test')
@@ -55,7 +54,7 @@ t.create('license for package with a license object')
     nock('https://registry.npmjs.org')
       .get('/package-license-object/latest')
       .reply(200, {
-        name: 'package-license-object',
+        label: 'package-license-object',
         license: {
           type: 'MIT',
           url: 'https://www.opensource.org/licenses/mit-license.php',
@@ -63,7 +62,7 @@ t.create('license for package with a license object')
         maintainers: [],
       })
   )
-  .expectJSON({ name: 'license', value: 'MIT', color: 'green' })
+  .expectBadge({ label: 'license', message: 'MIT', color: 'green' })
 
 t.create('license for package with a license array')
   .get('/package-license-array.json?style=_shields_test')
@@ -71,22 +70,22 @@ t.create('license for package with a license array')
     nock('https://registry.npmjs.org')
       .get('/package-license-array/latest')
       .reply(200, {
-        name: 'package-license-object',
+        label: 'package-license-object',
         license: ['MPL-2.0', 'MIT'],
         maintainers: [],
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'MPL-2.0, MIT',
+  .expectBadge({
+    label: 'license',
+    message: 'MPL-2.0, MIT',
     color: 'green',
   })
 
 t.create('license for unknown package')
   .get('/npm-registry-does-not-have-this-package.json?style=_shields_test')
-  .expectJSON({
-    name: 'license',
-    value: 'package not found',
+  .expectBadge({
+    label: 'license',
+    message: 'package not found',
     color: 'red',
   })
 
@@ -103,7 +102,7 @@ t.create('when json is malformed for scoped package')
         versions: null,
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'invalid json response',
+  .expectBadge({
+    label: 'license',
+    message: 'invalid json response',
   })
diff --git a/services/npm/npm-type-definitions.tester.js b/services/npm/npm-type-definitions.tester.js
index 5d1119d9aca20df0ae3a8eab0edd3c8a2506b991..2f2ae0cc375788ff24b27832e49bb22a70f588ea 100644
--- a/services/npm/npm-type-definitions.tester.js
+++ b/services/npm/npm-type-definitions.tester.js
@@ -9,22 +9,16 @@ const isTypeDefinition = Joi.string().regex(
 
 t.create('types (from dev dependencies + files)')
   .get('/chalk.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'types', value: isTypeDefinition })
-  )
+  .expectBadge({ label: 'types', message: isTypeDefinition })
 
 t.create('types (from files)')
   .get('/form-data-entries.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'types', value: isTypeDefinition })
-  )
+  .expectBadge({ label: 'types', message: isTypeDefinition })
 
 t.create('types (from types key)')
   .get('/left-pad.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'types', value: isTypeDefinition })
-  )
+  .expectBadge({ label: 'types', message: isTypeDefinition })
 
 t.create('no types')
   .get('/link-into.json')
-  .expectJSON({ name: 'types', value: 'none' })
+  .expectBadge({ label: 'types', message: 'none' })
diff --git a/services/npm/npm-version.tester.js b/services/npm/npm-version.tester.js
index 9c8c88eaac89d7110d6ef703ee2268fc398dfb04..f5545c4f3c997482a31d7fb89f9f7e015b0b2061 100644
--- a/services/npm/npm-version.tester.js
+++ b/services/npm/npm-version.tester.js
@@ -1,24 +1,23 @@
 'use strict'
 
-const Joi = require('joi')
 const { isSemver } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('gets the package version of left-pad')
   .get('/left-pad.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm', value: isSemver }))
+  .expectBadge({ label: 'npm', message: isSemver })
 
 t.create('gets the package version of left-pad from a custom registry')
   .get('/left-pad.json?registry_uri=https://registry.npmjs.com')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm', value: isSemver }))
+  .expectBadge({ label: 'npm', message: isSemver })
 
 t.create('gets the package version of @cycle/core')
   .get('/@cycle/core.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm', value: isSemver }))
+  .expectBadge({ label: 'npm', message: isSemver })
 
 t.create('gets a tagged package version of npm')
   .get('/npm/next.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm@next', value: isSemver }))
+  .expectBadge({ label: 'npm@next', message: isSemver })
 
 t.create('gets the correct tagged package version of npm')
   .intercept(nock =>
@@ -27,26 +26,26 @@ t.create('gets the correct tagged package version of npm')
       .reply(200, { latest: '1.2.3', next: '4.5.6' })
   )
   .get('/npm/next.json')
-  .expectJSON({ name: 'npm@next', value: 'v4.5.6' })
+  .expectBadge({ label: 'npm@next', message: 'v4.5.6' })
 
 t.create('returns an error for version with an invalid tag')
   .get('/npm/frodo.json')
-  .expectJSON({ name: 'npm', value: 'tag not found' })
+  .expectBadge({ label: 'npm', message: 'tag not found' })
 
 t.create('gets the package version of left-pad from a custom registry')
   .get('/left-pad.json?registry_uri=https://registry.npmjs.com')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm', value: isSemver }))
+  .expectBadge({ label: 'npm', message: isSemver })
 
 t.create('gets the tagged package version of @cycle/core')
   .get('/@cycle/core/canary.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm@canary', value: isSemver }))
+  .expectBadge({ label: 'npm@canary', message: isSemver })
 
 t.create(
   'gets the tagged package version of @cycle/core from a custom registry'
 )
   .get('/@cycle/core/canary.json?registry_uri=https://registry.npmjs.com')
-  .expectJSONTypes(Joi.object().keys({ name: 'npm@canary', value: isSemver }))
+  .expectBadge({ label: 'npm@canary', message: isSemver })
 
 t.create('invalid package name')
   .get('/frodo-is-not-a-package.json')
-  .expectJSON({ name: 'npm', value: 'package not found' })
+  .expectBadge({ label: 'npm', message: 'package not found' })
diff --git a/services/nsp/nsp.tester.js b/services/nsp/nsp.tester.js
index b2a66903edf9101035cb14f764d813b807f478cf..11a33eae1b87ce9113926e9cbd6e6edefb940aff 100644
--- a/services/nsp/nsp.tester.js
+++ b/services/nsp/nsp.tester.js
@@ -10,28 +10,28 @@ const t = (module.exports = new ServiceTester({
 
 t.create('no longer available (previously package)')
   .get('/gh-badges.json')
-  .expectJSON({
-    name: 'nsp',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'nsp',
+    message: 'no longer available',
   })
 
 t.create('no longer available (previously package version)')
   .get('/gh-badges/2.1.0.json')
-  .expectJSON({
-    name: 'nsp',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'nsp',
+    message: 'no longer available',
   })
 
 t.create('no longer available (previously scoped package)')
   .get('/@babel/core.json')
-  .expectJSON({
-    name: 'nsp',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'nsp',
+    message: 'no longer available',
   })
 
 t.create('no longer available (previously scoped package version)')
   .get('/@babel/core/7.1.6.json')
-  .expectJSON({
-    name: 'nsp',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'nsp',
+    message: 'no longer available',
   })
diff --git a/services/nuget/nuget.tester.js b/services/nuget/nuget.tester.js
index 8dc07a4812cfb27d25ed6184a7ddc05689add254..77b63bdfbddde1730cd19ea2c6d9a3dc36a46813 100644
--- a/services/nuget/nuget.tester.js
+++ b/services/nuget/nuget.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const {
   isMetric,
@@ -21,16 +20,14 @@ const t = (module.exports = new ServiceTester({ id: 'nuget', title: 'NuGet' }))
 
 t.create('total downloads (valid)')
   .get('/dt/Microsoft.AspNetCore.Mvc.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (not found)')
   .get('/dt/not-a-real-package.json')
-  .expectJSON({ name: 'downloads', value: 'package not found' })
+  .expectBadge({ label: 'downloads', message: 'package not found' })
 
 t.create('total downloads (unexpected second response)')
   .get('/dt/Microsoft.AspNetCore.Mvc.json')
@@ -46,18 +43,16 @@ t.create('total downloads (unexpected second response)')
       )
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'downloads', value: 'unparseable json response' })
+  .expectBadge({ label: 'downloads', message: 'unparseable json response' })
 
 // version
 
 t.create('version (valid)')
   .get('/v/Microsoft.AspNetCore.Mvc.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nuget',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'nuget',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (mocked, yellow badge)')
   .get('/v/Microsoft.AspNetCore.Mvc.json?style=_shields_test')
@@ -73,9 +68,9 @@ t.create('version (mocked, yellow badge)')
       )
       .reply(200, nuGetV3VersionJsonWithDash)
   )
-  .expectJSON({
-    name: 'nuget',
-    value: 'v1.2-beta',
+  .expectBadge({
+    label: 'nuget',
+    message: 'v1.2-beta',
     color: 'yellow',
   })
 
@@ -93,9 +88,9 @@ t.create('version (mocked, orange badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharZero)
   )
-  .expectJSON({
-    name: 'nuget',
-    value: 'v0.35',
+  .expectBadge({
+    label: 'nuget',
+    message: 'v0.35',
     color: 'orange',
   })
 
@@ -113,15 +108,15 @@ t.create('version (mocked, blue badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharNotZero)
   )
-  .expectJSON({
-    name: 'nuget',
-    value: 'v1.2.7',
+  .expectBadge({
+    label: 'nuget',
+    message: 'v1.2.7',
     color: 'blue',
   })
 
 t.create('version (not found)')
   .get('/v/not-a-real-package.json')
-  .expectJSON({ name: 'nuget', value: 'package not found' })
+  .expectBadge({ label: 'nuget', message: 'package not found' })
 
 t.create('version (unexpected second response)')
   .get('/v/Microsoft.AspNetCore.Mvc.json')
@@ -137,18 +132,16 @@ t.create('version (unexpected second response)')
       )
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'nuget', value: 'unparseable json response' })
+  .expectBadge({ label: 'nuget', message: 'unparseable json response' })
 
 // version (pre)
 
 t.create('version (pre) (valid)')
   .get('/vpre/Microsoft.AspNetCore.Mvc.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'nuget',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'nuget',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (pre) (mocked, yellow badge)')
   .get('/vpre/Microsoft.AspNetCore.Mvc.json?style=_shields_test')
@@ -164,9 +157,9 @@ t.create('version (pre) (mocked, yellow badge)')
       )
       .reply(200, nuGetV3VersionJsonWithDash)
   )
-  .expectJSON({
-    name: 'nuget',
-    value: 'v1.2-beta',
+  .expectBadge({
+    label: 'nuget',
+    message: 'v1.2-beta',
     color: 'yellow',
   })
 
@@ -184,9 +177,9 @@ t.create('version (pre) (mocked, orange badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharZero)
   )
-  .expectJSON({
-    name: 'nuget',
-    value: 'v0.35',
+  .expectBadge({
+    label: 'nuget',
+    message: 'v0.35',
     color: 'orange',
   })
 
@@ -204,15 +197,15 @@ t.create('version (pre) (mocked, blue badge)')
       )
       .reply(200, nuGetV3VersionJsonFirstCharNotZero)
   )
-  .expectJSON({
-    name: 'nuget',
-    value: 'v1.2.7',
+  .expectBadge({
+    label: 'nuget',
+    message: 'v1.2.7',
     color: 'blue',
   })
 
 t.create('version (pre) (not found)')
   .get('/vpre/not-a-real-package.json')
-  .expectJSON({ name: 'nuget', value: 'package not found' })
+  .expectBadge({ label: 'nuget', message: 'package not found' })
 
 t.create('version (pre) (unexpected second response)')
   .get('/vpre/Microsoft.AspNetCore.Mvc.json')
@@ -228,4 +221,4 @@ t.create('version (pre) (unexpected second response)')
       )
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'nuget', value: 'unparseable json response' })
+  .expectBadge({ label: 'nuget', message: 'unparseable json response' })
diff --git a/services/opencollective/opencollective-all.tester.js b/services/opencollective/opencollective-all.tester.js
index b7648f1dd98f893d38662af7d8c03b90c4a42dd8..4e0d8d192ab6aabfedf704db4b0e4b3ac6d557c7 100644
--- a/services/opencollective/opencollective-all.tester.js
+++ b/services/opencollective/opencollective-all.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { nonNegativeInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -20,43 +19,35 @@ t.create('renders correctly')
         contributorsCount: 276,
       })
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers and sponsors',
-      value: '35',
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'backers and sponsors',
+    message: '35',
+    color: 'brightgreen',
+  })
 t.create('gets amount of backers and sponsors')
   .get('/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers and sponsors',
-      value: nonNegativeInteger,
-    })
-  )
+  .expectBadge({
+    label: 'backers and sponsors',
+    message: nonNegativeInteger,
+  })
 
 t.create('renders not found correctly')
-  .get('/nonexistent-collective.json?style=_shield_test')
+  .get('/nonexistent-collective.json?style=_shields_test')
   .intercept(nock =>
     nock('https://opencollective.com/')
       .get('/nonexistent-collective.json')
       .reply(404, 'Not found')
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers and sponsors',
-      value: 'collective not found',
-      color: 'red',
-    })
-  )
+  .expectBadge({
+    label: 'backers and sponsors',
+    message: 'collective not found',
+    color: 'red',
+  })
 
 t.create('handles not found correctly')
-  .get('/nonexistent-collective.json?style=_shield_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers and sponsors',
-      value: 'collective not found',
-      color: 'red',
-    })
-  )
+  .get('/nonexistent-collective.json?style=_shields_test')
+  .expectBadge({
+    label: 'backers and sponsors',
+    message: 'collective not found',
+    color: 'red',
+  })
diff --git a/services/opencollective/opencollective-backers.tester.js b/services/opencollective/opencollective-backers.tester.js
index c6f16185ea5098e369c2b11fd87ce75646a6cfee..246829a1912d7ea3c04df4fe2d7bde8e6e018cdb 100644
--- a/services/opencollective/opencollective-backers.tester.js
+++ b/services/opencollective/opencollective-backers.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { nonNegativeInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -72,29 +71,23 @@ t.create('renders correctly')
         { MemberId: 27132, type: 'USER', role: 'CONTRIBUTOR' },
       ])
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers',
-      value: '25',
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'backers',
+    message: '25',
+    color: 'brightgreen',
+  })
 
 t.create('gets amount of backers')
   .get('/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers',
-      value: nonNegativeInteger,
-    })
-  )
+  .expectBadge({
+    label: 'backers',
+    message: nonNegativeInteger,
+  })
 
 t.create('handles not found correctly')
-  .get('/nonexistent-collective.json?style=_shield_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'backers',
-      value: 'collective not found',
-      color: 'red',
-    })
-  )
+  .get('/nonexistent-collective.json?style=_shields_test')
+  .expectBadge({
+    label: 'backers',
+    message: 'collective not found',
+    color: 'red',
+  })
diff --git a/services/opencollective/opencollective-by-tier.tester.js b/services/opencollective/opencollective-by-tier.tester.js
index e7f44cb05ce553df71a3c093315813fe78804ca4..b202281de4ad45e8e304424b465535f6b30844f0 100644
--- a/services/opencollective/opencollective-by-tier.tester.js
+++ b/services/opencollective/opencollective-by-tier.tester.js
@@ -1,11 +1,10 @@
 'use strict'
 
-const Joi = require('joi')
 const { nonNegativeInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('renders correctly')
-  .get('/shields/2988.json')
+  .get('/shields/2988.json?style=_shields_test')
   .intercept(nock =>
     nock('https://opencollective.com/')
       .get('/shields/members/all.json?TierId=2988')
@@ -60,45 +59,37 @@ t.create('renders correctly')
         },
       ])
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'monthly backers',
-      value: '8',
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'monthly backers',
+    message: '8',
+    color: 'brightgreen',
+  })
 
 // Not ideal, but open collective only returns an empty array
 t.create('shows 0 when given a non existent tier')
-  .get('/shields/1234567890.json')
+  .get('/shields/1234567890.json?style=_shields_test')
   .intercept(nock =>
     nock('https://opencollective.com/')
       .get('/shields/members/all.json?TierId=1234567890')
       .reply(200, [])
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'new tier',
-      value: '0',
-      color: 'lightgrey',
-    })
-  )
+  .expectBadge({
+    label: 'new tier',
+    message: '0',
+    color: 'lightgrey',
+  })
 
 t.create('gets amount of backers in specified tier')
   .get('/shields/2988.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'monthly backers',
-      value: nonNegativeInteger,
-    })
-  )
+  .expectBadge({
+    label: 'monthly backers',
+    message: nonNegativeInteger,
+  })
 
 t.create('handles not found correctly')
   .get('/nonexistent-collective/1234.json?style=_shields_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'open collective',
-      value: 'collective not found',
-      color: 'red',
-    })
-  )
+  .expectBadge({
+    label: 'open collective',
+    message: 'collective not found',
+    color: 'red',
+  })
diff --git a/services/opencollective/opencollective-sponsors.tester.js b/services/opencollective/opencollective-sponsors.tester.js
index 1a39cf358f79f1dfd0bb803e31302d073f104f31..34ab73d30b8f97974c3ac6484ab3feb7fdfded53 100644
--- a/services/opencollective/opencollective-sponsors.tester.js
+++ b/services/opencollective/opencollective-sponsors.tester.js
@@ -1,11 +1,10 @@
 'use strict'
 
-const Joi = require('joi')
 const { nonNegativeInteger } = require('../validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('renders correctly')
-  .get('/shields.json')
+  .get('/shields.json?style=_shields_test')
   .intercept(nock =>
     nock('https://opencollective.com/')
       .get('/shields/members/organizations.json')
@@ -64,28 +63,22 @@ t.create('renders correctly')
         },
       ])
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'sponsors',
-      value: '10',
-      color: 'brightgreen',
-    })
-  )
+  .expectBadge({
+    label: 'sponsors',
+    message: '10',
+    color: 'brightgreen',
+  })
 t.create('gets amount of sponsors')
   .get('/shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'sponsors',
-      value: nonNegativeInteger,
-    })
-  )
+  .expectBadge({
+    label: 'sponsors',
+    message: nonNegativeInteger,
+  })
 
 t.create('handles not found correctly')
-  .get('/nonexistent-collective.json?style=_shield_test')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'sponsors',
-      value: 'collective not found',
-      color: 'red',
-    })
-  )
+  .get('/nonexistent-collective.json?style=_shields_test')
+  .expectBadge({
+    label: 'sponsors',
+    message: 'collective not found',
+    color: 'red',
+  })
diff --git a/services/osslifecycle/osslifecycle.tester.js b/services/osslifecycle/osslifecycle.tester.js
index 56a0f1722f58b9b383107e66ca09f3cc9d615395..12ceeeb17d2c2de5a74ddb442d61bb910f3cb118 100644
--- a/services/osslifecycle/osslifecycle.tester.js
+++ b/services/osslifecycle/osslifecycle.tester.js
@@ -9,16 +9,16 @@ const t = (module.exports = new ServiceTester({
 
 t.create('osslifecycle status')
   .get('/Netflix/osstracker.json')
-  .expectJSON({
-    name: 'oss lifecycle',
-    value: 'active',
+  .expectBadge({
+    label: 'oss lifecycle',
+    message: 'active',
   })
 
 t.create('osslifecycle status (branch)')
   .get('/Netflix/osstracker/documentation.json')
-  .expectJSON({
-    name: 'oss lifecycle',
-    value: 'active',
+  .expectBadge({
+    label: 'oss lifecycle',
+    message: 'active',
   })
 
 t.create('oss metadata in unexpected format')
@@ -32,14 +32,14 @@ t.create('oss metadata in unexpected format')
       'Content-Type': 'text/plain;charset=UTF-8',
     }
   )
-  .expectJSON({
-    name: 'oss lifecycle',
-    value: 'metadata in unexpected format',
+  .expectBadge({
+    label: 'oss lifecycle',
+    message: 'metadata in unexpected format',
   })
 
 t.create('oss metadata not found')
   .get('/PyvesB/empty-repo.json')
-  .expectJSON({
-    name: 'oss lifecycle',
-    value: 'not found',
+  .expectBadge({
+    label: 'oss lifecycle',
+    message: 'not found',
   })
diff --git a/services/packagecontrol/packagecontrol.tester.js b/services/packagecontrol/packagecontrol.tester.js
index cdfdfb4fa9aec9cc35c3154e7356472fb745e614..4b29a5ad9ee9c6a447e0dff0e73683dc8b570904 100644
--- a/services/packagecontrol/packagecontrol.tester.js
+++ b/services/packagecontrol/packagecontrol.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric, isMetricOverTimePeriod } = require('../test-validators')
 
@@ -11,43 +10,35 @@ const t = (module.exports = new ServiceTester({
 
 t.create('monthly downloads')
   .get('/dm/GitGutter.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('weekly downloads')
   .get('/dw/GitGutter.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('daily downloads')
   .get('/dd/GitGutter.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('total downloads')
   .get('/dt/GitGutter.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('package not found')
   .get('/dt/does-not-exist.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'not found',
   })
diff --git a/services/packagist/packagist.tester.js b/services/packagist/packagist.tester.js
index f42abbae67b0e58b4c481bccebab15c09da0d041..63064aea8f26cc09c2b57e9f2a83231803a45b23 100644
--- a/services/packagist/packagist.tester.js
+++ b/services/packagist/packagist.tester.js
@@ -26,97 +26,89 @@ const t = (module.exports = new ServiceTester({
 
 t.create('gets the package version of symfony')
   .get('/php-v/symfony/symfony.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'php', value: isComposerVersion }))
+  .expectBadge({ label: 'php', message: isComposerVersion })
 
 t.create('gets the package version of symfony 2.8')
   .get('/php-v/symfony/symfony/v2.8.0.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'php', value: isComposerVersion }))
+  .expectBadge({ label: 'php', message: isComposerVersion })
 
 t.create('invalid package name')
   .get('/php-v/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'php', value: 'invalid' })
+  .expectBadge({ label: 'php', message: 'invalid' })
 
 // tests for download stats endpoints
 
 t.create('daily downloads (valid, no package version specified)')
   .get('/dd/doctrine/orm.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('monthly downloads (valid, no package version specified)')
   .get('/dm/doctrine/orm.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('total downloads (valid, no package version specified)')
   .get('/dt/doctrine/orm.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 // note: packagist can't give us download stats for a specific version
 t.create('daily downloads (invalid, package version specified)')
   .get('/dd/symfony/symfony/v2.8.0.json')
-  .expectJSON({ name: 'downloads', value: 'invalid' })
+  .expectBadge({ label: 'downloads', message: 'invalid' })
 
 t.create('monthly downloads (invalid, package version in request)')
   .get('/dm/symfony/symfony/v2.8.0.json')
-  .expectJSON({ name: 'downloads', value: 'invalid' })
+  .expectBadge({ label: 'downloads', message: 'invalid' })
 
 t.create('total downloads (invalid, package version in request)')
   .get('/dt/symfony/symfony/v2.8.0.json')
-  .expectJSON({ name: 'downloads', value: 'invalid' })
+  .expectBadge({ label: 'downloads', message: 'invalid' })
 
 t.create('daily downloads (invalid package name)')
   .get('/dd/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'invalid' })
+  .expectBadge({ label: 'downloads', message: 'invalid' })
 
 t.create('monthly downloads (invalid package name)')
   .get('/dm/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'invalid' })
+  .expectBadge({ label: 'downloads', message: 'invalid' })
 
 t.create('total downloads (invalid package name)')
   .get('/dt/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'invalid' })
+  .expectBadge({ label: 'downloads', message: 'invalid' })
 
 // tests for version endpoint
 
 t.create('version (valid)')
   .get('/v/symfony/symfony.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'packagist',
-      value: isPackagistVersion,
-    })
-  )
+  .expectBadge({
+    label: 'packagist',
+    message: isPackagistVersion,
+  })
 
 t.create('version (invalid package name)')
   .get('/v/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'packagist', value: 'invalid' })
+  .expectBadge({ label: 'packagist', message: 'invalid' })
 
 // tests for license endpoint
 
 t.create('license (valid)')
   .get('/l/symfony/symfony.json')
-  .expectJSON({ name: 'license', value: 'MIT' })
+  .expectBadge({ label: 'license', message: 'MIT' })
 
 // note: packagist does serve up license at the version level
 // but our endpoint only supports fetching license for the lastest version
 t.create('license (invalid, package version in request)')
   .get('/l/symfony/symfony/v2.8.0.json')
-  .expectJSON({ name: 'license', value: 'invalid' })
+  .expectBadge({ label: 'license', message: 'invalid' })
 
 t.create('license (invalid)')
   .get('/l/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'license', value: 'invalid' })
+  .expectBadge({ label: 'license', message: 'invalid' })
diff --git a/services/php-eye/php-eye-hhvm.tester.js b/services/php-eye/php-eye-hhvm.tester.js
index 1f5e6437d265da1254c80c9eb1db9aea90c50f68..a16a95b69b054e87f062a60fc201e59424517d9b 100644
--- a/services/php-eye/php-eye-hhvm.tester.js
+++ b/services/php-eye/php-eye-hhvm.tester.js
@@ -15,34 +15,30 @@ const t = (module.exports = new ServiceTester({
 
 t.create('get default branch')
   .get('/symfony/symfony.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'hhvm',
-      value: isAllowedStatus,
-    })
-  )
+  .expectBadge({
+    label: 'hhvm',
+    message: isAllowedStatus,
+  })
 
 t.create('get specific branch')
   .get('/yiisoft/yii/1.1.19.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'hhvm',
-      value: isAllowedStatus,
-    })
-  )
+  .expectBadge({
+    label: 'hhvm',
+    message: isAllowedStatus,
+  })
 
 t.create('invalid repo')
   .get('/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'hhvm', value: 'repo not found' })
+  .expectBadge({ label: 'hhvm', message: 'repo not found' })
 
 t.create('invalid branch')
   .get('/yiisoft/yii/1.1.666.json')
-  .expectJSON({ name: 'hhvm', value: 'branch not found' })
+  .expectBadge({ label: 'hhvm', message: 'branch not found' })
 
 t.create('connection error')
   .get('/symfony/symfony.json')
   .networkOff()
-  .expectJSON({ name: 'hhvm', value: 'inaccessible' })
+  .expectBadge({ label: 'hhvm', message: 'inaccessible' })
 
 t.create('unexpected response')
   .get('/symfony/symfony.json')
@@ -51,4 +47,4 @@ t.create('unexpected response')
       .get('/api/v1/package/symfony/symfony.json')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'hhvm', value: 'invalid' })
+  .expectBadge({ label: 'hhvm', message: 'invalid' })
diff --git a/services/php-eye/php-eye-php-version.tester.js b/services/php-eye/php-eye-php-version.tester.js
index 192a617265155c2cc8b7ba3f7758ab68a2c57ff0..5f2bdd3b8d4dd22c0e1158de0fbdafedd489be6d 100644
--- a/services/php-eye/php-eye-php-version.tester.js
+++ b/services/php-eye/php-eye-php-version.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isPhpVersionReduction } = require('../test-validators')
 
@@ -11,18 +10,16 @@ const t = (module.exports = new ServiceTester({
 
 t.create('gets the package version of symfony')
   .get('/symfony/symfony.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'php tested', value: isPhpVersionReduction })
-  )
+  .expectBadge({ label: 'php tested', message: isPhpVersionReduction })
 
 t.create('gets the package version of symfony 2.8')
   .get('/symfony/symfony/v2.8.0.json')
-  .expectJSON({ name: 'php tested', value: '5.3 - 7.0, HHVM' })
+  .expectBadge({ label: 'php tested', message: '5.3 - 7.0, HHVM' })
 
 t.create('gets the package version of yii')
   .get('/yiisoft/yii.json')
-  .expectJSON({ name: 'php tested', value: '5.3 - 7.1' })
+  .expectBadge({ label: 'php tested', message: '5.3 - 7.1' })
 
 t.create('invalid package name')
   .get('/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'php tested', value: 'invalid' })
+  .expectBadge({ label: 'php tested', message: 'invalid' })
diff --git a/services/powershellgallery/powershellgallery.tester.js b/services/powershellgallery/powershellgallery.tester.js
index 5f5e3bbd448384fe8e66094a00a91c4f18bbc450..abad1b6c74f499a594244ad300591a4a1d52f250 100644
--- a/services/powershellgallery/powershellgallery.tester.js
+++ b/services/powershellgallery/powershellgallery.tester.js
@@ -19,56 +19,48 @@ module.exports = t
 
 t.create('total downloads (valid)')
   .get('/dt/ACMESharp.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (not found)')
   .get('/dt/not-a-real-package.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 t.create('version (valid)')
   .get('/v/ACMESharp.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'powershell gallery',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'powershell gallery',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (not found)')
   .get('/v/not-a-real-package.json')
-  .expectJSON({ name: 'powershell gallery', value: 'not found' })
+  .expectBadge({ label: 'powershell gallery', message: 'not found' })
 
 t.create('version (pre) (valid)')
   .get('/vpre/ACMESharp.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'powershell gallery',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'powershell gallery',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (pre) (not found)')
   .get('/vpre/not-a-real-package.json')
-  .expectJSON({ name: 'powershell gallery', value: 'not found' })
+  .expectBadge({ label: 'powershell gallery', message: 'not found' })
 
 t.create('platform (valid')
   .get('/p/DNS.1.1.1.1.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'platform',
-      value: isPlatform,
-    })
-  )
+  .expectBadge({
+    label: 'platform',
+    message: isPlatform,
+  })
 
 t.create('platform (no tags)')
   .get('/p/ACMESharp.json')
-  .expectJSON({ name: 'platform', value: 'not specified' })
+  .expectBadge({ label: 'platform', message: 'not specified' })
 
 t.create('platform (not found)')
   .get('/p/not-a-real-package.json')
-  .expectJSON({ name: 'platform', value: 'not found' })
+  .expectBadge({ label: 'platform', message: 'not found' })
diff --git a/services/pub/pub.tester.js b/services/pub/pub.tester.js
index b1d58c14c3e0fb9b4bd0eb4861bb253bdfedb034..96bda4b97e9a00d178b462bce7a0c2e313f5a02c 100644
--- a/services/pub/pub.tester.js
+++ b/services/pub/pub.tester.js
@@ -1,30 +1,25 @@
 'use strict'
 
-const Joi = require('joi')
 const { isVPlusTripleDottedVersion } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('package version')
   .get('/v/box2d.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pub',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'pub',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('package pre-release version')
   .get('/vpre/box2d.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pub',
-      value: isVPlusTripleDottedVersion,
-    })
-  )
+  .expectBadge({
+    label: 'pub',
+    message: isVPlusTripleDottedVersion,
+  })
 
 t.create('package not found')
   .get('/v/does-not-exist.json')
-  .expectJSON({
-    name: 'pub',
-    value: 'not found',
+  .expectBadge({
+    label: 'pub',
+    message: 'not found',
   })
diff --git a/services/puppetforge/puppetforge-modules.tester.js b/services/puppetforge/puppetforge-modules.tester.js
index 8fcc1cf8990819a59e53bd924acdd6a79cce3219..577a4544d8ac7e39816cdd2a7cd0126223471711 100644
--- a/services/puppetforge/puppetforge-modules.tester.js
+++ b/services/puppetforge/puppetforge-modules.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isSemver } = require('../test-validators')
 
@@ -12,16 +11,14 @@ const t = (module.exports = new ServiceTester({
 
 t.create('PDK version')
   .get('/pdk-version/tragiccode/azure_key_vault.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pdk version',
-      value: isSemver,
-    })
-  )
+  .expectBadge({
+    label: 'pdk version',
+    message: isSemver,
+  })
 
 t.create("PDK version of a library that doesn't use the PDK")
   .get('/pdk-version/camptocamp/openssl.json')
-  .expectJSON({
-    name: 'pdk version',
-    value: 'none',
+  .expectBadge({
+    label: 'pdk version',
+    message: 'none',
   })
diff --git a/services/pypi/pypi.tester.js b/services/pypi/pypi.tester.js
index 93b1d2a09e515329f26f994c085470d708075d7b..8915cac77cf75ffee3ca7b975c1839784d980817 100644
--- a/services/pypi/pypi.tester.js
+++ b/services/pypi/pypi.tester.js
@@ -19,31 +19,31 @@ module.exports = t
 
 t.create('daily downloads (valid)')
   .get('/dd/djangorestframework.json')
-  .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod })
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('weekly downloads (valid)')
   .get('/dw/djangorestframework.json')
-  .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod })
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('monthly downloads (valid)')
   .get('/dm/djangorestframework.json')
-  .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod })
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('downloads (mixed-case package name)')
   .get('/dd/DjangoRestFramework.json')
-  .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod })
+  .expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
 
 t.create('daily downloads (not found)')
   .get('/dd/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'package not found' })
+  .expectBadge({ label: 'downloads', message: 'package not found' })
 
 t.create('weekly downloads (not found)')
   .get('/dw/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'package not found' })
+  .expectBadge({ label: 'downloads', message: 'package not found' })
 
 t.create('monthly downloads (not found)')
   .get('/dm/not-a-package.json')
-  .expectJSON({ name: 'downloads', value: 'package not found' })
+  .expectBadge({ label: 'downloads', message: 'package not found' })
 
 /*
   tests for version endpoint
@@ -61,26 +61,22 @@ t.create('monthly downloads (not found)')
 */
 t.create('version (semver)')
   .get('/v/requests.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pypi',
-      value: isSemver,
-    })
-  )
+  .expectBadge({
+    label: 'pypi',
+    message: isSemver,
+  })
 
 // ..whereas this project does not folow SemVer
 t.create('version (not semver)')
   .get('/v/psycopg2.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'pypi',
-      value: isPsycopg2Version,
-    })
-  )
+  .expectBadge({
+    label: 'pypi',
+    message: isPsycopg2Version,
+  })
 
 t.create('version (invalid)')
   .get('/v/not-a-package.json')
-  .expectJSON({ name: 'pypi', value: 'package or version not found' })
+  .expectBadge({ label: 'pypi', message: 'package or version not found' })
 
 t.create('no trove classifiers')
   .get('/v/mapi.json')
@@ -96,24 +92,24 @@ t.create('no trove classifiers')
         releases: {},
       })
   )
-  .expectJSON({
-    name: 'pypi',
-    value: 'v1.2.3',
+  .expectBadge({
+    label: 'pypi',
+    message: 'v1.2.3',
   })
 
 // tests for license endpoint
 
 t.create('license (valid, package version in request)')
   .get('/l/requests/2.18.4.json')
-  .expectJSON({ name: 'license', value: 'Apache 2.0' })
+  .expectBadge({ label: 'license', message: 'Apache 2.0' })
 
 t.create('license (valid, no package version specified)')
   .get('/l/requests.json')
-  .expectJSON({ name: 'license', value: 'Apache 2.0' })
+  .expectBadge({ label: 'license', message: 'Apache 2.0' })
 
 t.create('license (invalid)')
   .get('/l/not-a-package.json')
-  .expectJSON({ name: 'license', value: 'package or version not found' })
+  .expectBadge({ label: 'license', message: 'package or version not found' })
 
 t.create('license (from trove classifier)')
   .get('/l/mapi.json')
@@ -129,9 +125,9 @@ t.create('license (from trove classifier)')
         releases: {},
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'mit license',
+  .expectBadge({
+    label: 'license',
+    message: 'mit license',
   })
 
 t.create('license (as acronym from trove classifier)')
@@ -150,150 +146,145 @@ t.create('license (as acronym from trove classifier)')
         releases: {},
       })
   )
-  .expectJSON({
-    name: 'license',
-    value: 'GPL',
+  .expectBadge({
+    label: 'license',
+    message: 'GPL',
   })
 
 // tests for wheel endpoint
 
 t.create('wheel (has wheel, package version in request)')
   .get('/wheel/requests/2.18.4.json')
-  .expectJSON({ name: 'wheel', value: 'yes' })
+  .expectBadge({ label: 'wheel', message: 'yes' })
 
 t.create('wheel (has wheel, no package version specified)')
   .get('/wheel/requests.json')
-  .expectJSON({ name: 'wheel', value: 'yes' })
+  .expectBadge({ label: 'wheel', message: 'yes' })
 
 t.create('wheel (no wheel)')
   .get('/wheel/chai/1.1.2.json')
-  .expectJSON({ name: 'wheel', value: 'no' })
+  .expectBadge({ label: 'wheel', message: 'no' })
 
 t.create('wheel (invalid)')
   .get('/wheel/not-a-package.json')
-  .expectJSON({ name: 'wheel', value: 'package or version not found' })
+  .expectBadge({ label: 'wheel', message: 'package or version not found' })
 
 // tests for format endpoint
 
 t.create('format (wheel, package version in request)')
   .get('/format/requests/2.18.4.json')
-  .expectJSON({ name: 'format', value: 'wheel' })
+  .expectBadge({ label: 'format', message: 'wheel' })
 
 t.create('format (wheel, no package version specified)')
   .get('/format/requests.json')
-  .expectJSON({ name: 'format', value: 'wheel' })
+  .expectBadge({ label: 'format', message: 'wheel' })
 
 t.create('format (source)')
   .get('/format/chai/1.1.2.json')
-  .expectJSON({ name: 'format', value: 'source' })
+  .expectBadge({ label: 'format', message: 'source' })
 
 t.create('format (egg)')
   .get('/format/virtualenv/0.8.2.json')
-  .expectJSON({ name: 'format', value: 'egg' })
+  .expectBadge({ label: 'format', message: 'egg' })
 
 t.create('format (invalid)')
   .get('/format/not-a-package.json')
-  .expectJSON({ name: 'format', value: 'package or version not found' })
+  .expectBadge({ label: 'format', message: 'package or version not found' })
 
 // tests for pyversions endpoint
 
 t.create('python versions (valid, package version in request)')
   .get('/pyversions/requests/2.18.4.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'python',
-      value: isPipeSeparatedPythonVersions,
-    })
-  )
+  .expectBadge({
+    label: 'python',
+    message: isPipeSeparatedPythonVersions,
+  })
 
 t.create('python versions (valid, no package version specified)')
   .get('/pyversions/requests.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'python',
-      value: isPipeSeparatedPythonVersions,
-    })
-  )
+  .expectBadge({
+    label: 'python',
+    message: isPipeSeparatedPythonVersions,
+  })
 
 t.create('python versions ("Only" and others)')
   .get('/pyversions/uvloop/0.12.1.json')
-  .expectJSON({ name: 'python', value: '3.5 | 3.6 | 3.7' })
+  .expectBadge({ label: 'python', message: '3.5 | 3.6 | 3.7' })
 
 t.create('python versions ("Only" only)')
   .get('/pyversions/hashpipe/0.9.1.json')
-  .expectJSON({ name: 'python', value: '3' })
+  .expectBadge({ label: 'python', message: '3' })
 
 t.create('python versions (no versions specified)')
   .get('/pyversions/pyshp/1.2.12.json')
-  .expectJSON({ name: 'python', value: 'missing' })
+  .expectBadge({ label: 'python', message: 'missing' })
 
 t.create('python versions (invalid)')
   .get('/pyversions/not-a-package.json')
-  .expectJSON({ name: 'python', value: 'package or version not found' })
+  .expectBadge({ label: 'python', message: 'package or version not found' })
 
 // tests for django versions endpoint
 
 t.create('supported django versions (valid, package version in request)')
   .get('/djversions/djangorestframework/3.7.3.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'django versions',
-      value: isPipeSeparatedDjangoVersions,
-    })
-  )
+  .expectBadge({
+    label: 'django versions',
+    message: isPipeSeparatedDjangoVersions,
+  })
 
 t.create('supported django versions (valid, no package version specified)')
   .get('/djversions/djangorestframework.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'django versions',
-      value: isPipeSeparatedDjangoVersions,
-    })
-  )
+  .expectBadge({
+    label: 'django versions',
+    message: isPipeSeparatedDjangoVersions,
+  })
 
 t.create('supported django versions (no versions specified)')
   .get('/djversions/django/1.11.json')
-  .expectJSON({ name: 'django versions', value: 'missing' })
+  .expectBadge({ label: 'django versions', message: 'missing' })
 
 t.create('supported django versions (invalid)')
   .get('/djversions/not-a-package.json')
-  .expectJSON({
-    name: 'django versions',
-    value: 'package or version not found',
+  .expectBadge({
+    label: 'django versions',
+    message: 'package or version not found',
   })
 
 // tests for implementation endpoint
 
 t.create('implementation (valid, package version in request)')
   .get('/implementation/beehive/1.0.json')
-  .expectJSON({ name: 'implementation', value: 'cpython | jython | pypy' })
+  .expectBadge({ label: 'implementation', message: 'cpython | jython | pypy' })
 
 t.create('implementation (valid, no package version specified)')
   .get('/implementation/numpy.json')
-  .expectJSON({ name: 'implementation', value: 'cpython' })
+  .expectBadge({ label: 'implementation', message: 'cpython' })
 
 t.create('implementation (not specified)')
   .get('/implementation/chai/1.1.2.json')
-  .expectJSON({ name: 'implementation', value: 'cpython' })
+  .expectBadge({ label: 'implementation', message: 'cpython' })
 
 t.create('implementation (invalid)')
   .get('/implementation/not-a-package.json')
-  .expectJSON({ name: 'implementation', value: 'package or version not found' })
+  .expectBadge({
+    label: 'implementation',
+    message: 'package or version not found',
+  })
 
 // tests for status endpoint
 
 t.create('status (valid, stable, package version in request)')
   .get('/status/django/1.11.json')
-  .expectJSON({ name: 'status', value: 'stable' })
+  .expectBadge({ label: 'status', message: 'stable' })
 
 t.create('status (valid, no package version specified)')
   .get('/status/typing.json')
-  .expectJSON({ name: 'status', value: 'stable' })
+  .expectBadge({ label: 'status', message: 'stable' })
 
 t.create('status (valid, beta)')
   .get('/status/django/2.0rc1.json')
-  .expectJSON({ name: 'status', value: 'beta' })
+  .expectBadge({ label: 'status', message: 'beta' })
 
 t.create('status (invalid)')
   .get('/status/not-a-package.json')
-  .expectJSON({ name: 'status', value: 'package or version not found' })
+  .expectBadge({ label: 'status', message: 'package or version not found' })
diff --git a/services/readthedocs/readthedocs.tester.js b/services/readthedocs/readthedocs.tester.js
index 2258f34e9d88d4ca5997879eaab6f1d4156bf33d..2ed6c8bdb45c437f8c7cf51bd42d1fe8519cd1df 100644
--- a/services/readthedocs/readthedocs.tester.js
+++ b/services/readthedocs/readthedocs.tester.js
@@ -6,39 +6,33 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('build status')
   .get('/pip.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docs',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'docs',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build status for named version')
   .get('/pip/stable.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docs',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'docs',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build status for named semantic version')
   .get('/scrapy/1.0.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'docs',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'docs',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build status for nonexistent version')
   // This establishes that the version is being sent.
   .get('/pip/foobar-is-not-a-version.json')
-  .expectJSON({
-    name: 'docs',
-    value: 'project or version not found',
+  .expectBadge({
+    label: 'docs',
+    message: 'project or version not found',
   })
 
 t.create('unknown project')
   .get('/this-repo/does-not-exist.json')
-  .expectJSON({ name: 'docs', value: 'project or version not found' })
+  .expectBadge({ label: 'docs', message: 'project or version not found' })
diff --git a/services/redmine/redmine.tester.js b/services/redmine/redmine.tester.js
index b1da1ec9891b33f082a2d49294d8d1e4f73a843a..720d8a77e819dbae61446d5f765c401d987cd963 100644
--- a/services/redmine/redmine.tester.js
+++ b/services/redmine/redmine.tester.js
@@ -11,27 +11,21 @@ const t = (module.exports = new ServiceTester({
 
 t.create('plugin rating')
   .get('/plugin/rating/redmine_xlsx_format_issue_exporter.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: Joi.string().regex(/^[0-9]+\.[0-9]+\/5\.0$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: Joi.string().regex(/^[0-9]+\.[0-9]+\/5\.0$/),
+  })
 
 t.create('plugin stars')
   .get('/plugin/stars/redmine_xlsx_format_issue_exporter.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: isStarRating,
+  })
 
 t.create('plugin not found')
   .get('/plugin/rating/plugin_not_found.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'redmine',
-      value: 'not found',
-    })
-  )
+  .expectBadge({
+    label: 'redmine',
+    message: 'not found',
+  })
diff --git a/services/requires/requires.tester.js b/services/requires/requires.tester.js
index 946bc736d92b17ff1f7f30dc026bd8f97421b2c8..92545be604469e1fce369c22f784ba850f05ce21 100644
--- a/services/requires/requires.tester.js
+++ b/services/requires/requires.tester.js
@@ -9,22 +9,18 @@ const isRequireStatus = Joi.string().regex(
 
 t.create('requirements (valid, without branch)')
   .get('/github/celery/celery.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'requirements',
-      value: isRequireStatus,
-    })
-  )
+  .expectBadge({
+    label: 'requirements',
+    message: isRequireStatus,
+  })
 
 t.create('requirements (valid, with branch)')
   .get('/github/celery/celery/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'requirements',
-      value: isRequireStatus,
-    })
-  )
+  .expectBadge({
+    label: 'requirements',
+    message: isRequireStatus,
+  })
 
 t.create('requirements (not found)')
   .get('/github/PyvesB/EmptyRepo.json')
-  .expectJSON({ name: 'requirements', value: 'not found' })
+  .expectBadge({ label: 'requirements', message: 'not found' })
diff --git a/services/resharper/resharper.tester.js b/services/resharper/resharper.tester.js
index 9f0d4461873da1735d297b5f5f85dc60417ed92e..63eec00ebae302659740a2b09a26c9533ce08d69 100644
--- a/services/resharper/resharper.tester.js
+++ b/services/resharper/resharper.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const {
   isMetric,
@@ -17,43 +16,37 @@ const t = (module.exports = new ServiceTester({
 
 t.create('total downloads (valid)')
   .get('/dt/ReSharper.Nuke.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('total downloads (not found)')
   .get('/dt/not-a-real-package.json')
-  .expectJSON({ name: 'downloads', value: 'not found' })
+  .expectBadge({ label: 'downloads', message: 'not found' })
 
 // version
 
 t.create('version (valid)')
   .get('/v/ReSharper.Nuke.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'resharper',
-      value: isVPlusDottedVersionNClauses,
-    })
-  )
+  .expectBadge({
+    label: 'resharper',
+    message: isVPlusDottedVersionNClauses,
+  })
 
 t.create('version (not found)')
   .get('/v/not-a-real-package.json')
-  .expectJSON({ name: 'resharper', value: 'not found' })
+  .expectBadge({ label: 'resharper', message: 'not found' })
 
 // version (pre)
 
 t.create('version (pre) (valid)')
   .get('/vpre/ReSharper.Nuke.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'resharper',
-      value: isVPlusDottedVersionNClausesWithOptionalSuffix,
-    })
-  )
+  .expectBadge({
+    label: 'resharper',
+    message: isVPlusDottedVersionNClausesWithOptionalSuffix,
+  })
 
 t.create('version (pre) (not found)')
   .get('/vpre/not-a-real-package.json')
-  .expectJSON({ name: 'resharper', value: 'not found' })
+  .expectBadge({ label: 'resharper', message: 'not found' })
diff --git a/services/scrutinizer/scrutinizer.tester.js b/services/scrutinizer/scrutinizer.tester.js
index 38205c098c665eaf8c24e1a4ad3ae6c202c5b34c..53b2ee696323ac97ef38441955554e76ae0e1001 100644
--- a/services/scrutinizer/scrutinizer.tester.js
+++ b/services/scrutinizer/scrutinizer.tester.js
@@ -12,70 +12,58 @@ const t = (module.exports = new ServiceTester({
 
 t.create('code quality')
   .get('/g/filp/whoops.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'code quality',
-      value: Joi.number().positive(),
-    })
-  )
+  .expectBadge({
+    label: 'code quality',
+    message: Joi.number().positive(),
+  })
 
 t.create('code quality (branch)')
   .get('/g/phpmyadmin/phpmyadmin/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'code quality',
-      value: Joi.number().positive(),
-    })
-  )
+  .expectBadge({
+    label: 'code quality',
+    message: Joi.number().positive(),
+  })
 
 t.create('code coverage')
   .get('/coverage/g/filp/whoops.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('code coverage (branch)')
   .get('/coverage/g/PHPMailer/PHPMailer/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('build')
   .get('/build/g/filp/whoops.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build (branch)')
   .get('/build/g/phpmyadmin/phpmyadmin/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('project not found')
   .get('/build/g/does-not-exist/does-not-exist.json')
-  .expectJSON({
-    name: 'build',
-    value: 'project or branch not found',
+  .expectBadge({
+    label: 'build',
+    message: 'project or branch not found',
   })
 
 t.create('code coverage unknown')
   .get('/coverage/g/phpmyadmin/phpmyadmin/master.json')
-  .expectJSON({
-    name: 'coverage',
-    value: 'unknown',
+  .expectBadge({
+    label: 'coverage',
+    message: 'unknown',
   })
 
 t.create('unexpected response data')
@@ -85,9 +73,9 @@ t.create('unexpected response data')
       .get('/api/repositories/g/filp/whoops')
       .reply(200, '{"unexpected":"data"}')
   )
-  .expectJSON({
-    name: 'coverage',
-    value: 'invalid',
+  .expectBadge({
+    label: 'coverage',
+    message: 'invalid',
   })
 
 t.create('build - unknown')
@@ -106,8 +94,8 @@ t.create('build - unknown')
         },
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'unknown',
+  .expectBadge({
+    label: 'build',
+    message: 'unknown',
     color: 'lightgrey',
   })
diff --git a/services/shippable/shippable.tester.js b/services/shippable/shippable.tester.js
index 10278ae1a2c3300ef4e4d24d020571209013f3d8..3f072dc28fbe6f38f4e6ff3aa9bcd5c0760f76fe 100644
--- a/services/shippable/shippable.tester.js
+++ b/services/shippable/shippable.tester.js
@@ -1,34 +1,29 @@
 'use strict'
 
-const Joi = require('joi')
 const { isBuildStatus } = require('../build-status')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('build status (valid, without branch)')
   .get('/5444c5ecb904a4b21567b0ff.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('build status (valid, with branch)')
   .get('/5444c5ecb904a4b21567b0ff/master.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: isBuildStatus,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: isBuildStatus,
+  })
 
 t.create('build status (branch not found)')
   .get('/5444c5ecb904a4b21567b0ff/not-a-branch.json')
-  .expectJSON({ name: 'shippable', value: 'branch not found' })
+  .expectBadge({ label: 'shippable', message: 'branch not found' })
 
 t.create('build status (not found)')
   .get('/not-a-build.json')
-  .expectJSON({ name: 'shippable', value: 'not found' })
+  .expectBadge({ label: 'shippable', message: 'not found' })
 
 t.create('build status (unexpected status code)')
   .get('/5444c5ecb904a4b21567b0ff.json')
@@ -37,4 +32,4 @@ t.create('build status (unexpected status code)')
       .get('/projects/5444c5ecb904a4b21567b0ff/branchRunStatus')
       .reply(200, '[{ "branchName": "master", "statusCode": 63 }]')
   )
-  .expectJSON({ name: 'shippable', value: 'invalid response data' })
+  .expectBadge({ label: 'shippable', message: 'invalid response data' })
diff --git a/services/snap-ci/snap-ci.tester.js b/services/snap-ci/snap-ci.tester.js
index 194966fc5b9546df0400c18a12f6368d3508deab..fb9659ee3e17b55fbc6a21c733c6be5d52663bc4 100644
--- a/services/snap-ci/snap-ci.tester.js
+++ b/services/snap-ci/snap-ci.tester.js
@@ -7,7 +7,7 @@ module.exports = t
 
 t.create('no longer available (previously build state)')
   .get('/snap-ci/ThoughtWorksStudios/eb_deployer/master.json')
-  .expectJSON({
-    name: 'snap ci',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'snap ci',
+    message: 'no longer available',
   })
diff --git a/services/snyk/snyk-vulnerability-github.tester.js b/services/snyk/snyk-vulnerability-github.tester.js
index 825b780c2f6ecf7a82b941b366be13a529b120e0..edb43f5cae4e74df50bfb4051bd6a2a8a7a29f2f 100644
--- a/services/snyk/snyk-vulnerability-github.tester.js
+++ b/services/snyk/snyk-vulnerability-github.tester.js
@@ -10,32 +10,34 @@ const {
 t.create('live: valid repo')
   .get('/badges/shields.json')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vulnerabilities',
-      value: Joi.number().required(),
-    })
-  )
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: Joi.number().required(),
+  })
 
 t.create('live: non existent repo')
   .get('/badges/not-real.json')
   .timeout(10000)
-  .expectJSON({ name: 'vulnerabilities', value: 'repo or manifest not found' })
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: 'repo or manifest not found',
+  })
 
 t.create('live: valid target manifest path')
   .get('/badges/shields/gh-badges/package.json.json')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vulnerabilities',
-      value: Joi.number().required(),
-    })
-  )
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: Joi.number().required(),
+  })
 
 t.create('live: invalid target manifest path')
   .get('/badges/shields/gh-badges/requirements.txt.json')
   .timeout(10000)
-  .expectJSON({ name: 'vulnerabilities', value: 'repo or manifest not found' })
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: 'repo or manifest not found',
+  })
 
 t.create('repo has no vulnerabilities')
   .get('/badges/shields.json?style=_shields_test')
@@ -44,9 +46,9 @@ t.create('repo has no vulnerabilities')
       .get('/badge.svg')
       .reply(200, zeroVulnerabilitiesSvg)
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '0',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '0',
     color: 'brightgreen',
   })
 
@@ -57,9 +59,9 @@ t.create('repo has vulnerabilities')
       .get('/badge.svg')
       .reply(200, twoVulnerabilitiesSvg)
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '2',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '2',
     color: 'red',
   })
 
@@ -73,9 +75,9 @@ t.create('target manifest file has no vulnerabilities')
       })
       .reply(200, zeroVulnerabilitiesSvg)
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '0',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '0',
     color: 'brightgreen',
   })
 
@@ -89,8 +91,8 @@ t.create('target manifest file has vulnerabilities')
       })
       .reply(200, twoVulnerabilitiesSvg)
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '2',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '2',
     color: 'red',
   })
diff --git a/services/snyk/snyk-vulnerability-npm.tester.js b/services/snyk/snyk-vulnerability-npm.tester.js
index 1b083a5f60c89c10245f654374777fadc076ba7b..47cef0b4d71196030cec514f8d9cdfbfba0e0d7b 100644
--- a/services/snyk/snyk-vulnerability-npm.tester.js
+++ b/services/snyk/snyk-vulnerability-npm.tester.js
@@ -10,46 +10,42 @@ const {
 t.create('live: valid package latest version')
   .get('/mocha.json')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vulnerabilities',
-      value: Joi.number().required(),
-    })
-  )
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: Joi.number().required(),
+  })
 
 t.create('live: valid scoped package latest version')
   .get('/@babel/core.json')
   .timeout(10000)
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vulnerabilities',
-      value: Joi.number().required(),
-    })
-  )
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: Joi.number().required(),
+  })
 
 t.create('live: non existent package')
   .get('/mochaabcdef.json')
   .timeout(10000)
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: 'npm package is invalid or does not exist',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: 'npm package is invalid or does not exist',
   })
 
 t.create('live: valid package specific version')
   .get('/mocha@4.0.0.json?style=_shields_test')
   .timeout(10000)
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '1',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '1',
     color: 'red',
   })
 
 t.create('live: non existent package version')
   .get('/gh-badges@0.3.4.json')
   .timeout(10000)
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: 'npm package is invalid or does not exist',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: 'npm package is invalid or does not exist',
   })
 
 t.create('package has no vulnerabilities')
@@ -59,9 +55,9 @@ t.create('package has no vulnerabilities')
       .get('/badge.svg')
       .reply(200, zeroVulnerabilitiesSvg)
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '0',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '0',
     color: 'brightgreen',
   })
 
@@ -72,9 +68,9 @@ t.create('package has vulnerabilities')
       .get('/badge.svg')
       .reply(200, twoVulnerabilitiesSvg)
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: '2',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: '2',
     color: 'red',
   })
 
@@ -85,8 +81,8 @@ t.create('package not found')
       .get('/badge.svg')
       .reply(200, '<html>foo</html>')
   )
-  .expectJSON({
-    name: 'vulnerabilities',
-    value: 'npm package is invalid or does not exist',
+  .expectBadge({
+    label: 'vulnerabilities',
+    message: 'npm package is invalid or does not exist',
     color: 'red',
   })
diff --git a/services/sonarqube/sonarqube.tester.js b/services/sonarqube/sonarqube.tester.js
index bcd64af3b0f839c176d6024293053178ec87beb3..884b5014629f75130d186664053b9e0fe530c6a6 100644
--- a/services/sonarqube/sonarqube.tester.js
+++ b/services/sonarqube/sonarqube.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isIntegerPercentage } = require('../test-validators')
 
@@ -11,55 +10,47 @@ t.create('Tech Debt')
   .get(
     '/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/tech_debt.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tech debt',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'tech debt',
+    message: isIntegerPercentage,
+  })
 
 t.create('Coverage')
   .get(
     '/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/coverage.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('Tech Debt (legacy API supported)')
   .get(
     '/4.2/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/tech_debt.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tech debt',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'tech debt',
+    message: isIntegerPercentage,
+  })
 
 t.create('Coverage (legacy API supported)')
   .get(
     '/4.2/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/coverage.json'
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('Tech Debt (legacy API unsupported)')
   .timeout(15000)
   .get(
     '/4.2/https/sonarqube.com/com.github.dannil:scb-java-client/tech_debt.json'
   )
-  .expectJSON({ name: 'tech debt', value: 'invalid' })
+  .expectBadge({ label: 'tech debt', message: 'invalid' })
 
 t.create('Coverage (legacy API unsupported)')
   .get(
     '/4.2/https/sonarqube.com/com.github.dannil:scb-java-client/coverage.json'
   )
-  .expectJSON({ name: 'coverage', value: 'invalid' })
+  .expectBadge({ label: 'coverage', message: 'invalid' })
diff --git a/services/sourceforge/sourceforge.tester.js b/services/sourceforge/sourceforge.tester.js
index 1bbe841c495878171ca59e50f62b648f654f670f..36191c3eeb8a11bec67fb2337b95b49f2213bcb4 100644
--- a/services/sourceforge/sourceforge.tester.js
+++ b/services/sourceforge/sourceforge.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric, isMetricOverTimePeriod } = require('../test-validators')
 
@@ -9,51 +8,43 @@ module.exports = t
 
 t.create('total downloads')
   .get('/dt/sevenzip.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('monthly downloads')
   .get('/dm/sevenzip.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('weekly downloads')
   .get('/dw/sevenzip.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('daily downloads')
   .get('/dd/sevenzip.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('invalid project')
   .get('/dd/invalid.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'invalid',
+  .expectBadge({
+    label: 'downloads',
+    message: 'invalid',
   })
 
 t.create('total downloads (connection error)')
   .get('/dt/sevenzip.json')
   .networkOff()
-  .expectJSON({
-    name: 'downloads',
-    value: 'inaccessible',
+  .expectBadge({
+    label: 'downloads',
+    message: 'inaccessible',
   })
diff --git a/services/sourcegraph/sourcegraph.tester.js b/services/sourcegraph/sourcegraph.tester.js
index 69c4fcc73cc2d44b0f5d212c9c1b7041384b65d4..ae4b135bd3e440b5ee4cebe105b3cbb2485adcaa 100644
--- a/services/sourcegraph/sourcegraph.tester.js
+++ b/services/sourcegraph/sourcegraph.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -10,16 +9,14 @@ const projectsCount = withRegex(/^[0-9]*(\.[0-9]k)?\sprojects$/)
 
 t.create('project usage count')
   .get('/github.com/theupdateframework/notary.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'used by',
-      value: projectsCount,
-    })
-  )
+  .expectBadge({
+    label: 'used by',
+    message: projectsCount,
+  })
 
 t.create('project without any available information')
   .get('/github.com/PyvesB/EmptyRepo.json')
-  .expectJSON({
-    name: 'used by',
-    value: '0 projects',
+  .expectBadge({
+    label: 'used by',
+    message: '0 projects',
   })
diff --git a/services/spiget/spiget-download-size.tester.js b/services/spiget/spiget-download-size.tester.js
index 2df1792d8ffa11b8b47236a966ff9a6f1b447ae8..bdcdd3bdecf6b43960bbdbef3610e2320ae7ab69 100644
--- a/services/spiget/spiget-download-size.tester.js
+++ b/services/spiget/spiget-download-size.tester.js
@@ -1,16 +1,15 @@
 'use strict'
 
-const Joi = require('joi')
 const { isFileSize } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('EssentialsX (id 9089)')
   .get('/9089.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'size', value: isFileSize }))
+  .expectBadge({ label: 'size', message: isFileSize })
 
 t.create('Invalid Resource (id 1)')
   .get('/1.json')
-  .expectJSON({
-    name: 'size',
-    value: 'not found',
+  .expectBadge({
+    label: 'size',
+    message: 'not found',
   })
diff --git a/services/spiget/spiget-downloads.tester.js b/services/spiget/spiget-downloads.tester.js
index 50dcd50b779e892e1faa76bf96fdad353b0ad72a..fb2f7a2d8a67116f4925dfd62b2d9da320d21dd8 100644
--- a/services/spiget/spiget-downloads.tester.js
+++ b/services/spiget/spiget-downloads.tester.js
@@ -1,21 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('EssentialsX (id 9089)')
   .get('/9089.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('Invalid Resource (id 1)')
   .get('/1.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'not found',
   })
diff --git a/services/spiget/spiget-latest-version.tester.js b/services/spiget/spiget-latest-version.tester.js
index 92b15c5bc13ee135b1fd804bafea244e60224cc7..c088f072c62d9c77ce4ebfe07917f2bc3f895741 100644
--- a/services/spiget/spiget-latest-version.tester.js
+++ b/services/spiget/spiget-latest-version.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -8,16 +7,14 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('EssentialsX (id 9089)')
   .get('/9089.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'spiget',
-      value: withRegex(/^(?!not found$)/),
-    })
-  )
+  .expectBadge({
+    label: 'spiget',
+    message: withRegex(/^(?!not found$)/),
+  })
 
 t.create('Invalid Resource (id 1)')
   .get('/1.json')
-  .expectJSON({
-    name: 'spiget',
-    value: 'not found',
+  .expectBadge({
+    label: 'spiget',
+    message: 'not found',
   })
diff --git a/services/spiget/spiget-rating.tester.js b/services/spiget/spiget-rating.tester.js
index e9c8de22f1ada23b5c55d26524214e492e98d0c1..fd238c0adbd950a423e9191c358f80f63d78168b 100644
--- a/services/spiget/spiget-rating.tester.js
+++ b/services/spiget/spiget-rating.tester.js
@@ -1,37 +1,32 @@
 'use strict'
 
-const Joi = require('joi')
 const { isStarRating, withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Stars - EssentialsX (id 9089)')
   .get('/stars/9089.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
 
 t.create('Stars - Invalid Resource (id 1)')
   .get('/stars/1.json')
-  .expectJSON({
-    name: 'rating',
-    value: 'not found',
+  .expectBadge({
+    label: 'rating',
+    message: 'not found',
   })
 
 t.create('Rating - EssentialsX (id 9089)')
   .get('/rating/9089.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: withRegex(/^(\d*\.\d+)(\/5 \()(\d+)(\))$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: withRegex(/^(\d*\.\d+)(\/5 \()(\d+)(\))$/),
+  })
 
 t.create('Rating - Invalid Resource (id 1)')
   .get('/rating/1.json')
-  .expectJSON({
-    name: 'rating',
-    value: 'not found',
+  .expectBadge({
+    label: 'rating',
+    message: 'not found',
   })
diff --git a/services/spiget/spiget-tested-versions.tester.js b/services/spiget/spiget-tested-versions.tester.js
index a6301fee7755d88de43f65ade6ed09f0001988ab..cf942beabede1efb2d1af0395bb61abeff14be23 100644
--- a/services/spiget/spiget-tested-versions.tester.js
+++ b/services/spiget/spiget-tested-versions.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { withRegex } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
@@ -8,18 +7,16 @@ const multipleVersions = withRegex(/^([+]?\d*\.\d+)(-)([+]?\d*\.\d+)$/)
 
 t.create('EssentialsX - multiple versions supported - (id 9089)')
   .get('/9089.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tested versions',
-      value: multipleVersions,
-    })
-  )
+  .expectBadge({
+    label: 'tested versions',
+    message: multipleVersions,
+  })
 
 t.create('Invalid Resource (id 1)')
   .get('/1.json')
-  .expectJSON({
-    name: 'tested versions',
-    value: 'not found',
+  .expectBadge({
+    label: 'tested versions',
+    message: 'not found',
   })
 
 t.create('Nock - single version supported')
@@ -40,9 +37,9 @@ t.create('Nock - single version supported')
         },
       })
   )
-  .expectJSON({
-    name: 'tested versions',
-    value: '1.13',
+  .expectBadge({
+    label: 'tested versions',
+    message: '1.13',
   })
 
 t.create('Nock - multiple versions supported')
@@ -63,7 +60,7 @@ t.create('Nock - multiple versions supported')
         },
       })
   )
-  .expectJSON({
-    name: 'tested versions',
-    value: '1.10-1.13',
+  .expectBadge({
+    label: 'tested versions',
+    message: '1.10-1.13',
   })
diff --git a/services/stackexchange/stackexchange-monthlyquestions.tester.js b/services/stackexchange/stackexchange-monthlyquestions.tester.js
index ed5b122b034229df9f98426b5e2747b50afd1eaa..88f8b16622c31645eac7268dab9810e29b21184e 100644
--- a/services/stackexchange/stackexchange-monthlyquestions.tester.js
+++ b/services/stackexchange/stackexchange-monthlyquestions.tester.js
@@ -1,23 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetricOverTimePeriod } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Monthly Questions for StackOverflow Momentjs')
   .get('/stackoverflow/qm/momentjs.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stackoverflow momentjs questions',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'stackoverflow momentjs questions',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('Monthly Questions for Tex Spacing')
   .get('/tex/qm/spacing.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tex spacing questions',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'tex spacing questions',
+    message: isMetricOverTimePeriod,
+  })
diff --git a/services/stackexchange/stackexchange-reputation.tester.js b/services/stackexchange/stackexchange-reputation.tester.js
index eb2df7a5f5d08a454b2d31d42a9a7e591cd450b9..18a1f8144af7463ca8e90fc5124590fca50da496 100644
--- a/services/stackexchange/stackexchange-reputation.tester.js
+++ b/services/stackexchange/stackexchange-reputation.tester.js
@@ -1,27 +1,22 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Invalid parameters')
   .get('/stackoverflow/r/invalidimage.json')
-  .expectJSON({ name: 'stackoverflow', value: 'invalid parameters' })
+  .expectBadge({ label: 'stackoverflow', message: 'invalid parameters' })
 
 t.create('Reputation for StackOverflow user 22656')
   .get('/stackoverflow/r/22656.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stackoverflow reputation',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'stackoverflow reputation',
+    message: isMetric,
+  })
 
 t.create('Reputation for Tex user 22656')
   .get('/tex/r/226.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tex reputation',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'tex reputation',
+    message: isMetric,
+  })
diff --git a/services/stackexchange/stackexchange-taginfo.tester.js b/services/stackexchange/stackexchange-taginfo.tester.js
index df159961d38a76036f00d673636fcb928618df94..e80b518ce3a0c2c3d6458ac789dea490764e77d0 100644
--- a/services/stackexchange/stackexchange-taginfo.tester.js
+++ b/services/stackexchange/stackexchange-taginfo.tester.js
@@ -1,23 +1,18 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('JavaScript Questions')
   .get('/stackoverflow/t/javascript.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stackoverflow javascript questions',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'stackoverflow javascript questions',
+    message: isMetric,
+  })
 
 t.create('Tex Programming Questions')
   .get('/tex/t/programming.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'tex programming questions',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'tex programming questions',
+    message: isMetric,
+  })
diff --git a/services/static-badge/query-string-static.tester.js b/services/static-badge/query-string-static.tester.js
index 6701d703455c1ab0ab290a31bd857702e469f2e8..c754a97a85b933a81a1f334ee04352b85e5fa64e 100644
--- a/services/static-badge/query-string-static.tester.js
+++ b/services/static-badge/query-string-static.tester.js
@@ -4,36 +4,36 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Missing message')
   .get('/static/v1.json?label=label&message=&color=blue&style=_shields_test')
-  .expectJSON({
-    name: 'label',
-    value: 'invalid query parameter: message',
+  .expectBadge({
+    label: 'label',
+    message: 'invalid query parameter: message',
     color: 'red',
   })
 
 t.create('Missing label')
   .get('/static/v1.json?label=&message=message&color=blue&style=_shields_test')
-  .expectJSON({ name: '', value: 'message', color: 'blue' })
+  .expectBadge({ label: '', message: 'message', color: 'blue' })
 
 t.create('Case is preserved')
   .get(
     '/static/v1.json?label=LiCeNsE&message=mIt&color=blue&style=_shields_test'
   )
-  .expectJSON({ name: 'LiCeNsE', value: 'mIt', color: 'blue' })
+  .expectBadge({ label: 'LiCeNsE', message: 'mIt', color: 'blue' })
 
 t.create('Set color')
   .get(
     '/static/v1.json?label=label&message=message&color=yellow&style=_shields_test'
   )
-  .expectJSON({ name: 'label', value: 'message', color: 'yellow' })
+  .expectBadge({ label: 'label', message: 'message', color: 'yellow' })
 
 t.create('Set color with a number')
   .get(
     '/static/v1.json?label=label&message=message&color=123&style=_shields_test'
   )
-  .expectJSON({ name: 'label', value: 'message', color: '#123' })
+  .expectBadge({ label: 'label', message: 'message', color: '#123' })
 
 t.create('Set label')
   .get(
     '/static/v1.json?label=mylabel&message=message&color=blue&style=_shields_test'
   )
-  .expectJSON({ name: 'mylabel', value: 'message', color: 'blue' })
+  .expectBadge({ label: 'mylabel', message: 'message', color: 'blue' })
diff --git a/services/static-badge/static-badge.tester.js b/services/static-badge/static-badge.tester.js
index d702247fa3b5ac2d63ea0768cb0507166427d6f6..b1b8e538faeb435714a5a1da046cd8e2daa452e5 100644
--- a/services/static-badge/static-badge.tester.js
+++ b/services/static-badge/static-badge.tester.js
@@ -4,19 +4,23 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Shields colorscheme color')
   .get('/badge/label-message-blue.json?style=_shields_test')
-  .expectJSON({ name: 'label', value: 'message', color: 'blue' })
+  .expectBadge({ label: 'label', message: 'message', color: 'blue' })
 
 t.create('CSS named color')
   .get('/badge/label-message-whitesmoke.json?style=_shields_test')
-  .expectJSON({ name: 'label', value: 'message', color: 'whitesmoke' })
+  .expectBadge({ label: 'label', message: 'message', color: 'whitesmoke' })
 
 t.create('RGB color')
   .get('/badge/label-message-rgb(123,123,123).json?style=_shields_test')
-  .expectJSON({ name: 'label', value: 'message', color: 'rgb(123,123,123)' })
+  .expectBadge({
+    label: 'label',
+    message: 'message',
+    color: 'rgb(123,123,123)',
+  })
 
 t.create('All one color')
   .get('/badge/all%20one%20color-red.json?style=_shields_test')
-  .expectJSON({ name: '', value: 'all one color', color: 'red' })
+  .expectBadge({ label: '', message: 'all one color', color: 'red' })
 
 t.create('Not a valid color')
   .get('/badge/label-message-notacolor.json?style=_shields_test')
@@ -24,35 +28,35 @@ t.create('Not a valid color')
 
 t.create('Missing message')
   .get('/badge/label--blue.json?style=_shields_test')
-  .expectJSON({ name: 'label', value: '', color: 'blue' })
+  .expectBadge({ label: 'label', message: '', color: 'blue' })
 
 t.create('Missing label')
   .get('/badge/-message-blue.json?style=_shields_test')
-  .expectJSON({ name: '', value: 'message', color: 'blue' })
+  .expectBadge({ label: '', message: 'message', color: 'blue' })
 
 t.create('Case is preserved')
   .get('/badge/LiCeNsE-mIt-blue.json?style=_shields_test')
-  .expectJSON({ name: 'LiCeNsE', value: 'mIt', color: 'blue' })
+  .expectBadge({ label: 'LiCeNsE', message: 'mIt', color: 'blue' })
 
 t.create('"Shields-encoded" dash')
   .get('/badge/best--license-Apache--2.0-blue.json?style=_shields_test')
-  .expectJSON({ name: 'best-license', value: 'Apache-2.0', color: 'blue' })
+  .expectBadge({ label: 'best-license', message: 'Apache-2.0', color: 'blue' })
 
 t.create('Override color')
   .get('/badge/label-message-blue.json?style=_shields_test&color=yellow')
-  .expectJSON({ name: 'label', value: 'message', color: 'yellow' })
+  .expectBadge({ label: 'label', message: 'message', color: 'yellow' })
 
 t.create('Override color (legacy)')
   .get('/badge/label-message-blue.json?style=_shields_test&colorB=yellow')
-  .expectJSON({ name: 'label', value: 'message', color: 'yellow' })
+  .expectBadge({ label: 'label', message: 'message', color: 'yellow' })
 
 t.create('Override color with a number')
   .get('/badge/label-message-blue.json?style=_shields_test&color=123')
-  .expectJSON({ name: 'label', value: 'message', color: '#123' })
+  .expectBadge({ label: 'label', message: 'message', color: '#123' })
 
 t.create('Override label')
   .get('/badge/label-message-blue.json?style=_shields_test&label=mylabel')
-  .expectJSON({ name: 'mylabel', value: 'message', color: 'blue' })
+  .expectBadge({ label: 'mylabel', message: 'message', color: 'blue' })
 
 t.create('Old static badge')
   .get('/foo/bar.png?color=blue', { followRedirect: false })
diff --git a/services/steam/steam-workshop.tester.js b/services/steam/steam-workshop.tester.js
index 3114e39b5cd07fa4d7e0168a3b19e2c2c8d2bb10..82178e5f534ab85c5f640ded71664c670be6a7c4 100644
--- a/services/steam/steam-workshop.tester.js
+++ b/services/steam/steam-workshop.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isMetric, isFileSize, isFormattedDate } = require('../test-validators')
 
@@ -9,60 +8,56 @@ module.exports = t
 
 t.create('Collection Files')
   .get('/collection-files/180077636.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'files', value: isMetric }))
+  .expectBadge({ label: 'files', message: isMetric })
 
 t.create('File Size')
   .get('/size/1523924535.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'size', value: isFileSize }))
+  .expectBadge({ label: 'size', message: isFileSize })
 
 t.create('Release Date')
   .get('/release-date/1523924535.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'release date', value: isFormattedDate })
-  )
+  .expectBadge({ label: 'release date', message: isFormattedDate })
 
 t.create('Subscriptions')
   .get('/subscriptions/1523924535.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'subscriptions', value: isMetric })
-  )
+  .expectBadge({ label: 'subscriptions', message: isMetric })
 
 t.create('Favorites')
   .get('/favorites/1523924535.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'favorites', value: isMetric }))
+  .expectBadge({ label: 'favorites', message: isMetric })
 
 t.create('Downloads')
   .get('/downloads/1523924535.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'downloads', value: isMetric }))
+  .expectBadge({ label: 'downloads', message: isMetric })
 
 t.create('Views')
   .get('/views/1523924535.json')
-  .expectJSONTypes(Joi.object().keys({ name: 'views', value: isMetric }))
+  .expectBadge({ label: 'views', message: isMetric })
 
 t.create('Collection Files | Collection Not Found')
   .get('/collection-files/1.json')
-  .expectJSON({ name: 'files', value: 'collection not found' })
+  .expectBadge({ label: 'files', message: 'collection not found' })
 
 t.create('File Size | File Not Found')
   .get('/size/1.json')
-  .expectJSON({ name: 'size', value: 'file not found' })
+  .expectBadge({ label: 'size', message: 'file not found' })
 
 t.create('Release Date | File Not Found')
   .get('/release-date/1.json')
-  .expectJSON({ name: 'release date', value: 'file not found' })
+  .expectBadge({ label: 'release date', message: 'file not found' })
 
 t.create('Subscriptions | File Not Found')
   .get('/subscriptions/1.json')
-  .expectJSON({ name: 'subscriptions', value: 'file not found' })
+  .expectBadge({ label: 'subscriptions', message: 'file not found' })
 
 t.create('Favorites | File Not Found')
   .get('/favorites/1.json')
-  .expectJSON({ name: 'favorites', value: 'file not found' })
+  .expectBadge({ label: 'favorites', message: 'file not found' })
 
 t.create('Downloads | File Not Found')
   .get('/downloads/1.json')
-  .expectJSON({ name: 'downloads', value: 'file not found' })
+  .expectBadge({ label: 'downloads', message: 'file not found' })
 
 t.create('Views | File Not Found')
   .get('/views/1.json')
-  .expectJSON({ name: 'views', value: 'file not found' })
+  .expectBadge({ label: 'views', message: 'file not found' })
diff --git a/services/swagger/swagger.tester.js b/services/swagger/swagger.tester.js
index 941bcc8308f097ebcc61d27bedc01b99dd5ed559..e83086509a15e7d68f9c3229cfd4446387e99e1e 100644
--- a/services/swagger/swagger.tester.js
+++ b/services/swagger/swagger.tester.js
@@ -15,9 +15,9 @@ t.create('Valid (mocked)')
       .query(apiGetQueryParams)
       .reply(200, {})
   )
-  .expectJSON({
-    name: 'swagger',
-    value: 'valid',
+  .expectBadge({
+    label: 'swagger',
+    message: 'valid',
     color: 'brightgreen',
   })
 
@@ -36,8 +36,8 @@ t.create('Invalid (mocked)')
         ],
       })
   )
-  .expectJSON({
-    name: 'swagger',
-    value: 'invalid',
+  .expectBadge({
+    label: 'swagger',
+    message: 'invalid',
     color: 'red',
   })
diff --git a/services/symfony/symfony-insight-grade.tester.js b/services/symfony/symfony-insight-grade.tester.js
index 441c4b5cb34ca987afaf9260e1f1cc4948ef237b..9ed3c3a4c740cdd72c6ae9e1e9fc7a3ca653ec21 100644
--- a/services/symfony/symfony-insight-grade.tester.js
+++ b/services/symfony/symfony-insight-grade.tester.js
@@ -26,18 +26,16 @@ createTest(t, 'live: valid project grade', { withMockCreds: false })
       .get(`/${sampleProjectUuid}`)
       .reply(200, platinumMockResponse)
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'grade',
-      value: Joi.equal(
-        'platinum',
-        'gold',
-        'silver',
-        'bronze',
-        'no medal'
-      ).required(),
-    })
-  )
+  .expectBadge({
+    label: 'grade',
+    message: Joi.equal(
+      'platinum',
+      'gold',
+      'silver',
+      'bronze',
+      'no medal'
+    ).required(),
+  })
 
 createTest(t, 'live: nonexistent project', { withMockCreds: false })
   .before(prepLiveTest)
@@ -47,9 +45,9 @@ createTest(t, 'live: nonexistent project', { withMockCreds: false })
       .get('/45afb680-d4e6-4e66-93ea-bcfa79eb8a88')
       .reply(404)
   )
-  .expectJSON({
-    name: 'symfony insight',
-    value: 'project not found',
+  .expectBadge({
+    label: 'symfony insight',
+    message: 'project not found',
   })
 
 createTest(t, '401 not authorized grade')
@@ -59,9 +57,9 @@ createTest(t, '401 not authorized grade')
       .get(`/${sampleProjectUuid}`)
       .reply(401)
   )
-  .expectJSON({
-    name: 'symfony insight',
-    value: 'not authorized to access project',
+  .expectBadge({
+    label: 'symfony insight',
+    message: 'not authorized to access project',
   })
 
 createTest(t, 'pending project grade')
@@ -71,9 +69,9 @@ createTest(t, 'pending project grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, runningMockResponse)
   )
-  .expectJSON({
-    name: 'grade',
-    value: 'pending',
+  .expectBadge({
+    label: 'grade',
+    message: 'pending',
     color: 'lightgrey',
   })
 
@@ -84,9 +82,9 @@ createTest(t, 'platinum grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, platinumMockResponse)
   )
-  .expectJSON({
-    name: 'grade',
-    value: 'platinum',
+  .expectBadge({
+    label: 'grade',
+    message: 'platinum',
     color: '#e5e4e2',
   })
 
@@ -97,9 +95,9 @@ createTest(t, 'gold grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, goldMockResponse)
   )
-  .expectJSON({
-    name: 'grade',
-    value: 'gold',
+  .expectBadge({
+    label: 'grade',
+    message: 'gold',
     color: '#ebc760',
   })
 
@@ -110,9 +108,9 @@ createTest(t, 'silver grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, silverMockResponse)
   )
-  .expectJSON({
-    name: 'grade',
-    value: 'silver',
+  .expectBadge({
+    label: 'grade',
+    message: 'silver',
     color: '#c0c0c0',
   })
 
@@ -123,9 +121,9 @@ createTest(t, 'bronze grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, bronzeMockResponse)
   )
-  .expectJSON({
-    name: 'grade',
-    value: 'bronze',
+  .expectBadge({
+    label: 'grade',
+    message: 'bronze',
     color: '#c88f6a',
   })
 
@@ -136,17 +134,17 @@ createTest(t, 'no medal grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, noMedalMockResponse)
   )
-  .expectJSON({
-    name: 'grade',
-    value: 'no medal',
+  .expectBadge({
+    label: 'grade',
+    message: 'no medal',
     color: 'red',
   })
 
 createTest(t, 'auth missing', { withMockCreds: false })
   .before(setSymfonyInsightCredsToFalsy)
   .get(`/${sampleProjectUuid}.json`)
-  .expectJSON({
-    name: 'symfony insight',
-    value: 'required API tokens not found in config',
+  .expectBadge({
+    label: 'symfony insight',
+    message: 'required API tokens not found in config',
   })
   .after(restore)
diff --git a/services/symfony/symfony-insight-stars.tester.js b/services/symfony/symfony-insight-stars.tester.js
index 7f863a05c6e8afc20f553b99ee3f7eb6ed6f1f40..eab0931ad69229689aae4886e9fa224bce4e2e8f 100644
--- a/services/symfony/symfony-insight-stars.tester.js
+++ b/services/symfony/symfony-insight-stars.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { withRegex } = require('../test-validators')
 const {
@@ -25,14 +24,12 @@ createTest(t, 'live: valid project stars', { withMockCreds: false })
       .get(`/${sampleProjectUuid}`)
       .reply(200, platinumMockResponse)
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: withRegex(
-        /^(?=.{4}$)(\u2605{0,4}[\u00BC\u00BD\u00BE]?\u2606{0,4})$/
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: withRegex(
+      /^(?=.{4}$)(\u2605{0,4}[\u00BC\u00BD\u00BE]?\u2606{0,4})$/
+    ),
+  })
 
 createTest(t, 'live (stars): nonexistent project', { withMockCreds: false })
   .before(prepLiveTest)
@@ -42,9 +39,9 @@ createTest(t, 'live (stars): nonexistent project', { withMockCreds: false })
       .get('/abc')
       .reply(404)
   )
-  .expectJSON({
-    name: 'symfony insight',
-    value: 'project not found',
+  .expectBadge({
+    label: 'symfony insight',
+    message: 'project not found',
   })
 
 createTest(t, 'pending project stars')
@@ -54,9 +51,9 @@ createTest(t, 'pending project stars')
       .get(`/${sampleProjectUuid}`)
       .reply(200, runningMockResponse)
   )
-  .expectJSON({
-    name: 'stars',
-    value: 'pending',
+  .expectBadge({
+    label: 'stars',
+    message: 'pending',
     color: 'lightgrey',
   })
 
@@ -67,9 +64,9 @@ createTest(t, 'platinum stars')
       .get(`/${sampleProjectUuid}`)
       .reply(200, platinumMockResponse)
   )
-  .expectJSON({
-    name: 'stars',
-    value: '★★★★',
+  .expectBadge({
+    label: 'stars',
+    message: '★★★★',
     color: '#e5e4e2',
   })
 
@@ -80,9 +77,9 @@ createTest(t, 'gold stars')
       .get(`/${sampleProjectUuid}`)
       .reply(200, goldMockResponse)
   )
-  .expectJSON({
-    name: 'stars',
-    value: '★★★☆',
+  .expectBadge({
+    label: 'stars',
+    message: '★★★☆',
     color: '#ebc760',
   })
 
@@ -93,9 +90,9 @@ createTest(t, 'silver stars')
       .get(`/${sampleProjectUuid}`)
       .reply(200, silverMockResponse)
   )
-  .expectJSON({
-    name: 'stars',
-    value: '★★☆☆',
+  .expectBadge({
+    label: 'stars',
+    message: '★★☆☆',
     color: '#c0c0c0',
   })
 
@@ -106,9 +103,9 @@ createTest(t, 'bronze stars')
       .get(`/${sampleProjectUuid}`)
       .reply(200, bronzeMockResponse)
   )
-  .expectJSON({
-    name: 'stars',
-    value: '★☆☆☆',
+  .expectBadge({
+    label: 'stars',
+    message: '★☆☆☆',
     color: '#c88f6a',
   })
 
@@ -119,8 +116,8 @@ createTest(t, 'no medal stars')
       .get(`/${sampleProjectUuid}`)
       .reply(200, noMedalMockResponse)
   )
-  .expectJSON({
-    name: 'stars',
-    value: '☆☆☆☆',
+  .expectBadge({
+    label: 'stars',
+    message: '☆☆☆☆',
     color: 'red',
   })
diff --git a/services/symfony/symfony-insight-violations.tester.js b/services/symfony/symfony-insight-violations.tester.js
index e72f820ff895e4e81251be3e9c481d4b23908dd7..ddb87a9a478153f01ee0fa2895d51227eb024b1b 100644
--- a/services/symfony/symfony-insight-violations.tester.js
+++ b/services/symfony/symfony-insight-violations.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { withRegex } = require('../test-validators')
 const {
@@ -28,14 +27,12 @@ createTest(t, 'live: valid project violations', { withMockCreds: false })
       .get(`/${sampleProjectUuid}`)
       .reply(200, multipleViolations)
   )
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'violations',
-      value: withRegex(
-        /\d* critical|\d* critical, \d* major|\d* critical, \d* major, \d* minor|\d* critical, \d* major, \d* minor, \d* info|\d* critical, \d* minor|\d* critical, \d* info|\d* major|\d* major, \d* minor|\d* major, \d* minor, \d* info|\d* major, \d* info|\d* minor|\d* minor, \d* info/
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'violations',
+    message: withRegex(
+      /\d* critical|\d* critical, \d* major|\d* critical, \d* major, \d* minor|\d* critical, \d* major, \d* minor, \d* info|\d* critical, \d* minor|\d* critical, \d* info|\d* major|\d* major, \d* minor|\d* major, \d* minor, \d* info|\d* major, \d* info|\d* minor|\d* minor, \d* info/
+    ),
+  })
 
 createTest(t, 'pending project grade')
   .get(`/${sampleProjectUuid}.json?style=_shields_test`)
@@ -44,9 +41,9 @@ createTest(t, 'pending project grade')
       .get(`/${sampleProjectUuid}`)
       .reply(200, runningMockResponse)
   )
-  .expectJSON({
-    name: 'violations',
-    value: 'pending',
+  .expectBadge({
+    label: 'violations',
+    message: 'pending',
     color: 'lightgrey',
   })
 
@@ -57,9 +54,9 @@ createTest(t, 'zero violations')
       .get(`/${sampleProjectUuid}`)
       .reply(200, goldMockResponse)
   )
-  .expectJSON({
-    name: 'violations',
-    value: '0',
+  .expectBadge({
+    label: 'violations',
+    message: '0',
     color: 'brightgreen',
   })
 
@@ -70,9 +67,9 @@ createTest(t, 'critical violations')
       .get(`/${sampleProjectUuid}`)
       .reply(200, criticalViolation)
   )
-  .expectJSON({
-    name: 'violations',
-    value: '1 critical',
+  .expectBadge({
+    label: 'violations',
+    message: '1 critical',
     color: 'red',
   })
 
@@ -83,9 +80,9 @@ createTest(t, 'major violations')
       .get(`/${sampleProjectUuid}`)
       .reply(200, majorViolation)
   )
-  .expectJSON({
-    name: 'violations',
-    value: '1 major',
+  .expectBadge({
+    label: 'violations',
+    message: '1 major',
     color: 'orange',
   })
 
@@ -100,9 +97,9 @@ createTest(t, 'minor violations')
       })
       .reply(200, minorViolation)
   )
-  .expectJSON({
-    name: 'violations',
-    value: '1 minor',
+  .expectBadge({
+    label: 'violations',
+    message: '1 minor',
     color: 'yellow',
   })
 
@@ -117,9 +114,9 @@ createTest(t, 'info violations')
       })
       .reply(200, infoViolation)
   )
-  .expectJSON({
-    name: 'violations',
-    value: '1 info',
+  .expectBadge({
+    label: 'violations',
+    message: '1 info',
     color: 'yellowgreen',
   })
 
@@ -134,8 +131,8 @@ createTest(t, 'multiple violations grade')
       })
       .reply(200, multipleViolations)
   )
-  .expectJSON({
-    name: 'violations',
-    value: '1 critical, 1 info',
+  .expectBadge({
+    label: 'violations',
+    message: '1 critical, 1 info',
     color: 'red',
   })
diff --git a/services/teamcity/teamcity-build.tester.js b/services/teamcity/teamcity-build.tester.js
index b16e1c92390149c116652b8f69f7600d37db4036..3478cca9e4a0968dd88e4bc6c1ef710438aefa87 100644
--- a/services/teamcity/teamcity-build.tester.js
+++ b/services/teamcity/teamcity-build.tester.js
@@ -15,34 +15,28 @@ const buildStatusTextRegex = /^success|failure|error|tests( failed: \d+( \(\d+ n
 
 t.create('live: codebetter unknown build')
   .get('/codebetter/btabc.json')
-  .expectJSON({ name: 'build', value: 'build not found' })
+  .expectBadge({ label: 'build', message: 'build not found' })
 
 t.create('live: codebetter known build')
   .get('/codebetter/IntelliJIdeaCe_JavaDecompilerEngineTests.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: buildStatusValues,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: buildStatusValues,
+  })
 
 t.create('live: simple status for known build')
   .get('/https/teamcity.jetbrains.com/s/bt345.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: buildStatusValues,
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: buildStatusValues,
+  })
 
 t.create('live: full status for known build')
   .get('/https/teamcity.jetbrains.com/e/bt345.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: withRegex(buildStatusTextRegex),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: withRegex(buildStatusTextRegex),
+  })
 
 t.create('codebetter success build')
   .get('/codebetter/bt123.json?style=_shields_test')
@@ -55,9 +49,9 @@ t.create('codebetter success build')
         statusText: 'Success',
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'passing',
+  .expectBadge({
+    label: 'build',
+    message: 'passing',
     color: 'brightgreen',
   })
 
@@ -72,9 +66,9 @@ t.create('codebetter failure build')
         statusText: 'Tests failed: 2',
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'failure',
+  .expectBadge({
+    label: 'build',
+    message: 'failure',
     color: 'red',
   })
 
@@ -89,9 +83,9 @@ t.create('simple build status with passed build')
         statusText: 'Tests passed: 100',
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'passing',
+  .expectBadge({
+    label: 'build',
+    message: 'passing',
     color: 'brightgreen',
   })
 
@@ -106,9 +100,9 @@ t.create('simple build status with failed build')
         statusText: 'Tests failed: 10 (2 new)',
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'failure',
+  .expectBadge({
+    label: 'build',
+    message: 'failure',
     color: 'red',
   })
 
@@ -123,9 +117,9 @@ t.create('full build status with passed build')
         statusText: 'Tests passed: 100, ignored: 3',
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'passing',
+  .expectBadge({
+    label: 'build',
+    message: 'passing',
     color: 'brightgreen',
   })
 
@@ -142,9 +136,9 @@ t.create('full build status with failed build')
         statusText: 'Tests failed: 10 (2 new), passed: 99',
       })
   )
-  .expectJSON({
-    name: 'build',
-    value: 'tests failed: 10 (2 new), passed: 99',
+  .expectBadge({
+    label: 'build',
+    message: 'tests failed: 10 (2 new), passed: 99',
     color: 'red',
   })
 
@@ -168,8 +162,8 @@ t.create('with auth')
       })
   )
   .finally(restore)
-  .expectJSON({
-    name: 'build',
-    value: 'tests failed: 1 (1 new), passed: 50246, ignored: 1, muted: 12',
+  .expectBadge({
+    label: 'build',
+    message: 'tests failed: 1 (1 new), passed: 50246, ignored: 1, muted: 12',
     color: 'red',
   })
diff --git a/services/teamcity/teamcity-coverage.tester.js b/services/teamcity/teamcity-coverage.tester.js
index 21dd95f0787009d3217087e9faff49e2c8705212..b156e63ba536bb355ea912a133cf5e0f2098b8c0 100644
--- a/services/teamcity/teamcity-coverage.tester.js
+++ b/services/teamcity/teamcity-coverage.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isIntegerPercentage } = require('../test-validators')
 const t = (module.exports = require('../tester').createServiceTester())
 const {
@@ -12,29 +11,25 @@ const {
 
 t.create('live: valid buildId')
   .get('/ReactJSNet_PullRequests.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('live: specified instance valid buildId')
   .get('/https/teamcity.jetbrains.com/ReactJSNet_PullRequests.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'coverage',
-      value: isIntegerPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'coverage',
+    message: isIntegerPercentage,
+  })
 
 t.create('live: invalid buildId')
   .get('/btABC999.json')
-  .expectJSON({ name: 'coverage', value: 'build not found' })
+  .expectBadge({ label: 'coverage', message: 'build not found' })
 
 t.create('live: specified instance invalid buildId')
   .get('/https/teamcity.jetbrains.com/btABC000.json')
-  .expectJSON({ name: 'coverage', value: 'build not found' })
+  .expectBadge({ label: 'coverage', message: 'build not found' })
 
 t.create('404 latest build error response')
   .get('/bt123.json')
@@ -44,7 +39,7 @@ t.create('404 latest build error response')
       .query({ guest: 1 })
       .reply(404)
   )
-  .expectJSON({ name: 'coverage', value: 'build not found' })
+  .expectBadge({ label: 'coverage', message: 'build not found' })
 
 t.create('no coverage data for build')
   .get('/bt234.json')
@@ -54,7 +49,7 @@ t.create('no coverage data for build')
       .query({ guest: 1 })
       .reply(200, { property: [] })
   )
-  .expectJSON({ name: 'coverage', value: 'no coverage data available' })
+  .expectBadge({ label: 'coverage', message: 'no coverage data available' })
 
 t.create('zero lines covered')
   .get('/bt345.json?style=_shields_test')
@@ -75,9 +70,9 @@ t.create('zero lines covered')
         ],
       })
   )
-  .expectJSON({
-    name: 'coverage',
-    value: '0%',
+  .expectBadge({
+    label: 'coverage',
+    message: '0%',
     color: 'red',
   })
 
@@ -108,8 +103,8 @@ t.create('with auth, lines covered')
       })
   )
   .finally(restore)
-  .expectJSON({
-    name: 'coverage',
-    value: '82%',
+  .expectBadge({
+    label: 'coverage',
+    message: '82%',
     color: 'yellowgreen',
   })
diff --git a/services/travis/travis-build.tester.js b/services/travis/travis-build.tester.js
index 2182cc3588710f6d56658da0ea305247e6efb798..80167d111dd8f404e49af3c0821da343e41019d2 100644
--- a/services/travis/travis-build.tester.js
+++ b/services/travis/travis-build.tester.js
@@ -14,25 +14,21 @@ const t = (module.exports = new ServiceTester({
 
 t.create('build status on default branch')
   .get('/rust-lang/rust.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build status on named branch')
   .get('/rust-lang/rust/stable.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('unknown repo')
   .get('/this-repo/does-not-exist.json')
-  .expectJSON({ name: 'build', value: 'unknown' })
+  .expectBadge({ label: 'build', message: 'unknown' })
 
 t.create('invalid svg response')
   .get('/foo/bar.json')
@@ -41,31 +37,27 @@ t.create('invalid svg response')
       .get('/foo/bar.svg')
       .reply(200)
   )
-  .expectJSON({ name: 'build', value: 'unparseable svg response' })
+  .expectBadge({ label: 'build', message: 'unparseable svg response' })
 
 // Travis (.com) CI
 
 t.create('build status on default branch')
   .get('/com/ivandelabeldad/rackian-gateway.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('build status on named branch')
   .get('/com/ivandelabeldad/rackian-gateway.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'build',
-      value: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
-    })
-  )
+  .expectBadge({
+    label: 'build',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  })
 
 t.create('unknown repo')
   .get('/com/this-repo/does-not-exist.json')
-  .expectJSON({ name: 'build', value: 'unknown' })
+  .expectBadge({ label: 'build', message: 'unknown' })
 
 t.create('invalid svg response')
   .get('/com/foo/bar.json')
@@ -74,4 +66,4 @@ t.create('invalid svg response')
       .get('/foo/bar.svg')
       .reply(200)
   )
-  .expectJSON({ name: 'build', value: 'unparseable svg response' })
+  .expectBadge({ label: 'build', message: 'unparseable svg response' })
diff --git a/services/travis/travis-php-version.tester.js b/services/travis/travis-php-version.tester.js
index 0b5f4c19e55c7c6e22d4eea107910753dec0ebea..e72aeb97ba05fbf81c16b5648717dc7f56551af7 100644
--- a/services/travis/travis-php-version.tester.js
+++ b/services/travis/travis-php-version.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isPhpVersionReduction } = require('../test-validators')
 const { ServiceTester } = require('../tester')
 
@@ -12,22 +11,16 @@ const t = (module.exports = new ServiceTester({
 
 t.create('gets the package version of symfony')
   .get('/php-v/symfony/symfony.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'php', value: isPhpVersionReduction })
-  )
+  .expectBadge({ label: 'php', message: isPhpVersionReduction })
 
 t.create('gets the package version of symfony 2.8')
   .get('/php-v/symfony/symfony/2.8.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'php', value: isPhpVersionReduction })
-  )
+  .expectBadge({ label: 'php', message: isPhpVersionReduction })
 
 t.create('gets the package version of yii')
   .get('/php-v/yiisoft/yii.json')
-  .expectJSONTypes(
-    Joi.object().keys({ name: 'php', value: isPhpVersionReduction })
-  )
+  .expectBadge({ label: 'php', message: isPhpVersionReduction })
 
 t.create('invalid package name')
   .get('/php-v/frodo/is-not-a-package.json')
-  .expectJSON({ name: 'php', value: 'invalid' })
+  .expectBadge({ label: 'php', message: 'invalid' })
diff --git a/services/twitter/twitter.tester.js b/services/twitter/twitter.tester.js
index 7e7aec08e7acfced6d8a3e0a6529660a0b775603..fb89be1fc734049f9a30b700c6379735d4923366 100644
--- a/services/twitter/twitter.tester.js
+++ b/services/twitter/twitter.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { isMetric } = require('../test-validators')
 const { ServiceTester } = require('../tester')
 
@@ -11,36 +10,30 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Followers')
   .get('/follow/shields_io.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'follow @shields_io',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'follow @shields_io',
+    message: isMetric,
+  })
 
 t.create('Followers - Custom Label')
   .get('/follow/shields_io.json?label=Follow')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'Follow',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'Follow',
+    message: isMetric,
+  })
 
 t.create('Invalid Username Specified')
   .get('/follow/invalidusernamethatshouldnotexist.json?label=Follow')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'Follow',
-      value: 'invalid user',
-    })
-  )
+  .expectBadge({
+    label: 'Follow',
+    message: 'invalid user',
+  })
 
 t.create('No connection')
   .get('/follow/shields_io.json?label=Follow')
   .networkOff()
-  .expectJSON({ name: 'Follow', value: 'inaccessible' })
+  .expectBadge({ label: 'Follow', message: 'inaccessible' })
 
 t.create('URL')
   .get('/url/https/shields.io.json')
-  .expectJSON({ name: 'tweet', value: '' })
+  .expectBadge({ label: 'tweet', message: '' })
diff --git a/services/uptimerobot/uptimerobot-ratio.tester.js b/services/uptimerobot/uptimerobot-ratio.tester.js
index da470198e25048eb69f6ef2a9349b532c0e5d051..c3b9912be27ff0a692bd4994d2786581b0ce13cf 100644
--- a/services/uptimerobot/uptimerobot-ratio.tester.js
+++ b/services/uptimerobot/uptimerobot-ratio.tester.js
@@ -1,35 +1,33 @@
 'use strict'
 
-const Joi = require('joi')
 const { isPercentage } = require('../test-validators')
 const { invalidJSON } = require('../response-fixtures')
 const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('Uptime Robot: Percentage (valid)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'uptime',
-      value: isPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'uptime',
+    message: isPercentage,
+  })
 
 t.create('Uptime Robot: Percentage (valid, with numberOfDays param)')
   .get('/7/m778918918-3e92c097147760ee39d02d36.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'uptime',
-      value: isPercentage,
-    })
-  )
+  .expectBadge({
+    label: 'uptime',
+    message: isPercentage,
+  })
 
 t.create('Uptime Robot: Percentage (invalid, correct format)')
   .get('/m777777777-333333333333333333333333.json')
-  .expectJSON({ name: 'uptime', value: 'api_key not found.' })
+  .expectBadge({ label: 'uptime', message: 'api_key not found.' })
 
 t.create('Uptime Robot: Percentage (invalid, incorrect format)')
   .get('/not-a-service.json')
-  .expectJSON({ name: 'uptime', value: 'must use a monitor-specific api key' })
+  .expectBadge({
+    label: 'uptime',
+    message: 'must use a monitor-specific api key',
+  })
 
 t.create('Uptime Robot: Percentage (unspecified error)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -38,7 +36,7 @@ t.create('Uptime Robot: Percentage (unspecified error)')
       .post('/v2/getMonitors')
       .reply(200, '{"stat": "fail"}')
   )
-  .expectJSON({ name: 'uptime', value: 'service error' })
+  .expectBadge({ label: 'uptime', message: 'service error' })
 
 t.create('Uptime Robot: Percentage (service unavailable)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -47,7 +45,7 @@ t.create('Uptime Robot: Percentage (service unavailable)')
       .post('/v2/getMonitors')
       .reply(503, '{"error": "oh noes!!"}')
   )
-  .expectJSON({ name: 'uptime', value: 'inaccessible' })
+  .expectBadge({ label: 'uptime', message: 'inaccessible' })
 
 t.create('Uptime Robot: Percentage (unexpected response, valid json)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -56,7 +54,7 @@ t.create('Uptime Robot: Percentage (unexpected response, valid json)')
       .post('/v2/getMonitors')
       .reply(200, '[]')
   )
-  .expectJSON({ name: 'uptime', value: 'invalid response data' })
+  .expectBadge({ label: 'uptime', message: 'invalid response data' })
 
 t.create('Uptime Robot: Percentage (unexpected response, invalid json)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -65,4 +63,4 @@ t.create('Uptime Robot: Percentage (unexpected response, invalid json)')
       .post('/v2/getMonitors')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'uptime', value: 'unparseable json response' })
+  .expectBadge({ label: 'uptime', message: 'unparseable json response' })
diff --git a/services/uptimerobot/uptimerobot-status.tester.js b/services/uptimerobot/uptimerobot-status.tester.js
index 2b733f43b096aa1a46db156ccd0c31c19ac5a56b..e6128498d70ed6bb28be3fcb72109c9da9d99844 100644
--- a/services/uptimerobot/uptimerobot-status.tester.js
+++ b/services/uptimerobot/uptimerobot-status.tester.js
@@ -14,20 +14,21 @@ const isUptimeStatus = Joi.string().valid(
 
 t.create('Uptime Robot: Status (valid)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'status',
-      value: isUptimeStatus,
-    })
-  )
+  .expectBadge({
+    label: 'status',
+    message: isUptimeStatus,
+  })
 
 t.create('Uptime Robot: Status (invalid, correct format)')
   .get('/m777777777-333333333333333333333333.json')
-  .expectJSON({ name: 'status', value: 'api_key not found.' })
+  .expectBadge({ label: 'status', message: 'api_key not found.' })
 
 t.create('Uptime Robot: Status (invalid, incorrect format)')
   .get('/not-a-service.json')
-  .expectJSON({ name: 'status', value: 'must use a monitor-specific api key' })
+  .expectBadge({
+    label: 'status',
+    message: 'must use a monitor-specific api key',
+  })
 
 t.create('Uptime Robot: Status (unspecified error)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -36,7 +37,7 @@ t.create('Uptime Robot: Status (unspecified error)')
       .post('/v2/getMonitors')
       .reply(200, '{"stat": "fail"}')
   )
-  .expectJSON({ name: 'status', value: 'service error' })
+  .expectBadge({ label: 'status', message: 'service error' })
 
 t.create('Uptime Robot: Status (service unavailable)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -45,7 +46,7 @@ t.create('Uptime Robot: Status (service unavailable)')
       .post('/v2/getMonitors')
       .reply(503, '{"error": "oh noes!!"}')
   )
-  .expectJSON({ name: 'status', value: 'inaccessible' })
+  .expectBadge({ label: 'status', message: 'inaccessible' })
 
 t.create('Uptime Robot: Status (unexpected response, valid json)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -54,7 +55,7 @@ t.create('Uptime Robot: Status (unexpected response, valid json)')
       .post('/v2/getMonitors')
       .reply(200, '[]')
   )
-  .expectJSON({ name: 'status', value: 'invalid response data' })
+  .expectBadge({ label: 'status', message: 'invalid response data' })
 
 t.create('Uptime Robot: Status (unexpected response, invalid json)')
   .get('/m778918918-3e92c097147760ee39d02d36.json')
@@ -63,4 +64,4 @@ t.create('Uptime Robot: Status (unexpected response, invalid json)')
       .post('/v2/getMonitors')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'status', value: 'unparseable json response' })
+  .expectBadge({ label: 'status', message: 'unparseable json response' })
diff --git a/services/vaadin-directory/vaadin-directory.tester.js b/services/vaadin-directory/vaadin-directory.tester.js
index dba19219674ba5ef4c30039affcea5050533d70f..a78a47e6b3faeda8dee71d40231d6c44944f1cb1 100644
--- a/services/vaadin-directory/vaadin-directory.tester.js
+++ b/services/vaadin-directory/vaadin-directory.tester.js
@@ -15,112 +15,92 @@ const t = (module.exports = new ServiceTester({
 
 t.create('stars of component displayed in star icons')
   .get('/star/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: isStarRating,
+  })
 
 t.create('stars of component displayed in star icons')
   .get('/stars/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'stars',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'stars',
+    message: isStarRating,
+  })
 
 t.create('publish status of the component')
   .get('/status/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vaadin directory',
-      value: Joi.equal(
-        'published',
-        'unpublished',
-        'incomplete',
-        'reported',
-        'suspended',
-        'deleted'
-      ),
-    })
-  )
+  .expectBadge({
+    label: 'vaadin directory',
+    message: Joi.equal(
+      'published',
+      'unpublished',
+      'incomplete',
+      'reported',
+      'suspended',
+      'deleted'
+    ),
+  })
 
 t.create('rating of the component (eg: 4.2/5)')
   .get('/rating/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: Joi.string().regex(/^\d\.\d\/5$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: Joi.string().regex(/^\d\.\d\/5$/),
+  })
 
 t.create('rating count of component')
   .get('/rc/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating count',
-      value: Joi.string().regex(/^\d+?\stotal$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating count',
+    message: Joi.string().regex(/^\d+?\stotal$/),
+  })
 
 t.create('rating count of component')
   .get('/rating-count/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating count',
-      value: Joi.string().regex(/^\d+?\stotal$/),
-    })
-  )
+  .expectBadge({
+    label: 'rating count',
+    message: Joi.string().regex(/^\d+?\stotal$/),
+  })
 
 t.create('latest version of the component (can have v prefixed or without)')
   .get('/v/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'latest ver',
-      value: isSemver,
-    })
-  )
+  .expectBadge({
+    label: 'latest ver',
+    message: isSemver,
+  })
 
 t.create('latest version of the component (can have v prefixed or without)')
   .get('/version/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'latest ver',
-      value: isSemver,
-    })
-  )
+  .expectBadge({
+    label: 'latest ver',
+    message: isSemver,
+  })
 
 t.create('latest release date of the component (format: yyyy-mm-dd)')
   .get('/rd/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'latest release date',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'latest release date',
+    message: isFormattedDate,
+  })
 
 t.create('latest release date of the component (format: yyyy-mm-dd)')
   .get('/release-date/vaadinvaadin-grid.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'latest release date',
-      value: isFormattedDate,
-    })
-  )
+  .expectBadge({
+    label: 'latest release date',
+    message: isFormattedDate,
+  })
 
 t.create('Invalid addon')
   .get('/stars/404.json')
-  .expectJSON({
-    name: 'vaadin directory',
-    value: 'not found',
+  .expectBadge({
+    label: 'vaadin directory',
+    message: 'not found',
   })
 
 t.create('No connection')
   .get('/stars/vaadinvaadin-grid.json')
   .networkOff()
-  .expectJSON({
-    name: 'vaadin directory',
-    value: 'inaccessible',
+  .expectBadge({
+    label: 'vaadin directory',
+    message: 'inaccessible',
   })
diff --git a/services/versioneye/versioneye.tester.js b/services/versioneye/versioneye.tester.js
index 9a6c4c21a6da5a7c942dc4605f4530569f6cfe78..8523ffa54bfdc7c47a9988fac171f05491bbe0cc 100644
--- a/services/versioneye/versioneye.tester.js
+++ b/services/versioneye/versioneye.tester.js
@@ -7,7 +7,7 @@ module.exports = t
 
 t.create('no longer available (previously dependencies status)')
   .get('/d/ruby/rails.json')
-  .expectJSON({
-    name: 'versioneye',
-    value: 'no longer available',
+  .expectBadge({
+    label: 'versioneye',
+    message: 'no longer available',
   })
diff --git a/services/visual-studio-marketplace/visual-studio-marketplace-azure-devops-installs.tester.js b/services/visual-studio-marketplace/visual-studio-marketplace-azure-devops-installs.tester.js
index ea4f597ac9f2a6a160768f6b16c41275ea67049b..0415bdababf3db34ef89f9cc262c3028ad709b95 100644
--- a/services/visual-studio-marketplace/visual-studio-marketplace-azure-devops-installs.tester.js
+++ b/services/visual-studio-marketplace/visual-studio-marketplace-azure-devops-installs.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { isMetric } = require('../test-validators')
 
@@ -32,39 +31,31 @@ const mockResponse = {
 
 t.create('live: Azure DevOps Extension total installs')
   .get('/total/swellaby.mirror-git-repository.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: isMetric,
+  })
 
 t.create('live: Azure DevOps Extension services installs')
   .get('/services/swellaby.mirror-git-repository.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: isMetric,
+  })
 
 t.create('live: invalid extension id')
   .get('/services/badges-shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: 'invalid extension id',
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: 'invalid extension id',
+  })
 
 t.create('live: non existent extension')
   .get('/total/badges.shields-io-fake.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: 'extension not found',
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: 'extension not found',
+  })
 
 t.create('total installs')
   .get('/total/swellaby.cobertura-transform.json?style=_shields_test')
@@ -73,9 +64,9 @@ t.create('total installs')
       .post(`/extensionquery/`)
       .reply(200, mockResponse)
   )
-  .expectJSON({
-    name: 'installs',
-    value: '28',
+  .expectBadge({
+    label: 'installs',
+    message: '28',
     color: 'yellowgreen',
   })
 
@@ -86,9 +77,9 @@ t.create('services installs')
       .post(`/extensionquery/`)
       .reply(200, mockResponse)
   )
-  .expectJSON({
-    name: 'installs',
-    value: '21',
+  .expectBadge({
+    label: 'installs',
+    message: '21',
     color: 'yellowgreen',
   })
 
@@ -99,9 +90,9 @@ t.create('onprem installs')
       .post(`/extensionquery/`)
       .reply(200, mockResponse)
   )
-  .expectJSON({
-    name: 'installs',
-    value: '7',
+  .expectBadge({
+    label: 'installs',
+    message: '7',
     color: 'yellow',
   })
 
@@ -127,8 +118,8 @@ t.create('zero installs')
         ],
       })
   )
-  .expectJSON({
-    name: 'installs',
-    value: '0',
+  .expectBadge({
+    label: 'installs',
+    message: '0',
     color: 'red',
   })
diff --git a/services/visual-studio-marketplace/visual-studio-marketplace-downloads.tester.js b/services/visual-studio-marketplace/visual-studio-marketplace-downloads.tester.js
index 76ed6db782cfb7e9df7eb7c794f596ae40273b5a..4c894135f67ab3a8136ff0f72dfe18c62e38a328 100644
--- a/services/visual-studio-marketplace/visual-studio-marketplace-downloads.tester.js
+++ b/services/visual-studio-marketplace/visual-studio-marketplace-downloads.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { isMetric } = require('../test-validators')
 
@@ -32,39 +31,31 @@ const mockResponse = {
 
 t.create('live: installs')
   .get('/visual-studio-marketplace/i/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: isMetric,
+  })
 
 t.create('live: downloads')
   .get('/visual-studio-marketplace/d/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 
 t.create('live: invalid extension id')
   .get('/visual-studio-marketplace/d/badges-shields.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vs marketplace',
-      value: 'invalid extension id',
-    })
-  )
+  .expectBadge({
+    label: 'vs marketplace',
+    message: 'invalid extension id',
+  })
 
 t.create('live: non existent extension')
   .get('/visual-studio-marketplace/d/badges.shields-io-fake.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'vs marketplace',
-      value: 'extension not found',
-    })
-  )
+  .expectBadge({
+    label: 'vs marketplace',
+    message: 'extension not found',
+  })
 
 t.create('installs')
   .get(
@@ -75,9 +66,9 @@ t.create('installs')
       .post(`/extensionquery/`)
       .reply(200, mockResponse)
   )
-  .expectJSON({
-    name: 'installs',
-    value: '3',
+  .expectBadge({
+    label: 'installs',
+    message: '3',
     color: 'yellow',
   })
 
@@ -105,9 +96,9 @@ t.create('zero installs')
         ],
       })
   )
-  .expectJSON({
-    name: 'installs',
-    value: '0',
+  .expectBadge({
+    label: 'installs',
+    message: '0',
     color: 'red',
   })
 
@@ -120,26 +111,22 @@ t.create('downloads')
       .post(`/extensionquery/`)
       .reply(200, mockResponse)
   )
-  .expectJSON({
-    name: 'downloads',
-    value: '10',
+  .expectBadge({
+    label: 'downloads',
+    message: '10',
     color: 'yellowgreen',
   })
 
 t.create('live: installs (legacy)')
   .get('/vscode-marketplace/i/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'installs',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'installs',
+    message: isMetric,
+  })
 
 t.create('live: downloads (legacy)')
   .get('/vscode-marketplace/d/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
diff --git a/services/visual-studio-marketplace/visual-studio-marketplace-rating.tester.js b/services/visual-studio-marketplace/visual-studio-marketplace-rating.tester.js
index 21593cf3aa120dd89b525a8e8d5acfe8826cab5e..85b9fdc869c5b276b491e5288be3de0c23ea5239 100644
--- a/services/visual-studio-marketplace/visual-studio-marketplace-rating.tester.js
+++ b/services/visual-studio-marketplace/visual-studio-marketplace-rating.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { withRegex, isStarRating } = require('../test-validators')
 
@@ -8,21 +7,17 @@ const isVscodeRating = withRegex(/[0-5]\.[0-9]{1}\/5?\s*\([0-9]*\)$/)
 
 t.create('live: rating')
   .get('/visual-studio-marketplace/r/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isVscodeRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isVscodeRating,
+  })
 
 t.create('live: stars')
   .get('/visual-studio-marketplace/stars/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
 
 t.create('rating')
   .get(
@@ -57,9 +52,9 @@ t.create('rating')
         ],
       })
   )
-  .expectJSON({
-    name: 'rating',
-    value: '2.5/5 (10)',
+  .expectBadge({
+    label: 'rating',
+    message: '2.5/5 (10)',
     color: 'yellowgreen',
   })
 
@@ -87,9 +82,9 @@ t.create('zero rating')
         ],
       })
   )
-  .expectJSON({
-    name: 'rating',
-    value: '0.0/5 (0)',
+  .expectBadge({
+    label: 'rating',
+    message: '0.0/5 (0)',
     color: 'red',
   })
 
@@ -126,26 +121,22 @@ t.create('stars')
         ],
       })
   )
-  .expectJSON({
-    name: 'rating',
-    value: '★★★★¾',
+  .expectBadge({
+    label: 'rating',
+    message: '★★★★¾',
     color: 'brightgreen',
   })
 
 t.create('live: rating (legacy)')
   .get('/vscode-marketplace/r/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isVscodeRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isVscodeRating,
+  })
 
 t.create('live: stars (legacy)')
   .get('/vscode-marketplace/stars/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
diff --git a/services/visual-studio-marketplace/visual-studio-marketplace-version.tester.js b/services/visual-studio-marketplace/visual-studio-marketplace-version.tester.js
index 84b956383bb2ed78e425457b92fac9381a3bdec8..488625d33f835b673d5bdd1286364d4de6a9f657 100644
--- a/services/visual-studio-marketplace/visual-studio-marketplace-version.tester.js
+++ b/services/visual-studio-marketplace/visual-studio-marketplace-version.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const t = (module.exports = require('../tester').createServiceTester())
 const { withRegex } = require('../test-validators')
 
@@ -8,12 +7,10 @@ const isMarketplaceVersion = withRegex(/^v(\d+\.\d+\.\d+)(\.\d+)?$/)
 
 t.create('live: rating')
   .get('/visual-studio-marketplace/v/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'version',
-      value: isMarketplaceVersion,
-    })
-  )
+  .expectBadge({
+    label: 'version',
+    message: isMarketplaceVersion,
+  })
 
 t.create('version')
   .get(
@@ -39,9 +36,9 @@ t.create('version')
         ],
       })
   )
-  .expectJSON({
-    name: 'version',
-    value: 'v1.0.0',
+  .expectBadge({
+    label: 'version',
+    message: 'v1.0.0',
     color: 'blue',
   })
 
@@ -69,17 +66,15 @@ t.create('pre-release version')
         ],
       })
   )
-  .expectJSON({
-    name: 'version',
-    value: 'v0.3.8',
+  .expectBadge({
+    label: 'version',
+    message: 'v0.3.8',
     color: 'orange',
   })
 
 t.create('live: version (legacy)')
   .get('/vscode-marketplace/v/ritwickdey.LiveServer.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'version',
-      value: isMarketplaceVersion,
-    })
-  )
+  .expectBadge({
+    label: 'version',
+    message: isMarketplaceVersion,
+  })
diff --git a/services/waffle/waffle.tester.js b/services/waffle/waffle.tester.js
index a4bdfc61369fd8211008460ba28f765a534bfe93..f19e3cda12e9e68ea1529545194f7df6b043b443 100644
--- a/services/waffle/waffle.tester.js
+++ b/services/waffle/waffle.tester.js
@@ -37,37 +37,35 @@ t.create(
       .get('/userName/repoName/columns?with=count')
       .reply(200, fakeData)
   )
-  .expectJSON({
-    name: 'bug',
-    value: '5',
+  .expectBadge({
+    label: 'bug',
+    message: '5',
     color: '#fbca04',
   })
 
 t.create('label should be `Mybug` & value should be formatted.  e.g: Mybug|25')
   .get('/ritwickdey/vscode-live-server/bug.json?label=Mybug')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'Mybug',
-      value: Joi.number()
-        .integer()
-        .positive(),
-    })
-  )
+  .expectBadge({
+    label: 'Mybug',
+    message: Joi.number()
+      .integer()
+      .positive(),
+  })
 
 t.create('label (repo not found)')
   .get('/not-a-user/not-a-repo/bug.json')
-  .expectJSON({
-    name: 'waffle',
-    value: 'not found',
+  .expectBadge({
+    label: 'waffle',
+    message: 'not found',
   })
 
 t.create('label (label not found)')
   .get(
     '/ritwickdey/vscode-live-server/not-a-real-label.json?style=_shields_test'
   )
-  .expectJSON({
-    name: 'not-a-real-label',
-    value: '0',
+  .expectBadge({
+    label: 'not-a-real-label',
+    message: '0',
     color: '#78bdf2',
   })
 
@@ -78,15 +76,15 @@ t.create('label (empty response)')
       .get('/userName/repoName/columns?with=count')
       .reply(200, [])
   )
-  .expectJSON({
-    name: 'waffle',
-    value: 'absent',
+  .expectBadge({
+    label: 'waffle',
+    message: 'absent',
   })
 
 t.create('label (connection error)')
   .get('/ritwickdey/vscode-live-server/bug.json')
   .networkOff()
-  .expectJSON({ name: 'waffle', value: 'inaccessible' })
+  .expectBadge({ label: 'waffle', message: 'inaccessible' })
 
 t.create('label (unexpected response)')
   .get('/userName/repoName/bug.json')
@@ -95,4 +93,4 @@ t.create('label (unexpected response)')
       .get('/userName/repoName/columns?with=count')
       .reply(invalidJSON)
   )
-  .expectJSON({ name: 'waffle', value: 'invalid' })
+  .expectBadge({ label: 'waffle', message: 'invalid' })
diff --git a/services/website/website.tester.js b/services/website/website.tester.js
index 0fa6b981fa6dbc3b2b9e769f5fb62b40c697e6a8..edfa297d7d57ef9d48fbb9dda67ae6806aa25529 100644
--- a/services/website/website.tester.js
+++ b/services/website/website.tester.js
@@ -4,20 +4,20 @@ const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('status of http://shields.io')
   .get('/website/http/shields.io.json?style=_shields_test')
-  .expectJSON({ name: 'website', value: 'online', color: 'brightgreen' })
+  .expectBadge({ label: 'website', message: 'online', color: 'brightgreen' })
 
 t.create('status of https://shields.io')
   .get('/website/https/shields.io.json?style=_shields_test')
-  .expectJSON({ name: 'website', value: 'online', color: 'brightgreen' })
+  .expectBadge({ label: 'website', message: 'online', color: 'brightgreen' })
 
 t.create('status of nonexistent domain')
   .get('/website/https/shields-io.io.json?style=_shields_test')
-  .expectJSON({ name: 'website', value: 'offline', color: 'red' })
+  .expectBadge({ label: 'website', message: 'offline', color: 'red' })
 
 t.create('status when network is off')
   .get('/website/http/shields.io.json?style=_shields_test')
   .networkOff()
-  .expectJSON({ name: 'website', value: 'offline', color: 'red' })
+  .expectBadge({ label: 'website', message: 'offline', color: 'red' })
 
 t.create('custom online label, online message and online color')
   .get(
@@ -28,7 +28,7 @@ t.create('custom online label, online message and online color')
       .head('/')
       .reply(200)
   )
-  .expectJSON({ name: 'homepage', value: 'up', color: 'green' })
+  .expectBadge({ label: 'homepage', message: 'up', color: 'green' })
 
 t.create('custom offline message and offline color')
   .get('/website-up-down-green-grey/http/offline.com.json?style=_shields_test')
@@ -37,4 +37,4 @@ t.create('custom offline message and offline color')
       .head('/')
       .reply(500)
   )
-  .expectJSON({ name: 'website', value: 'down', color: 'grey' })
+  .expectBadge({ label: 'website', message: 'down', color: 'grey' })
diff --git a/services/wheelmap/wheelmap.tester.js b/services/wheelmap/wheelmap.tester.js
index 4315080adf8b053dad4f02cab9d71f9858c6f11d..167c1a4dc9fd80b7f1f6387147ca9e352ece8e06 100644
--- a/services/wheelmap/wheelmap.tester.js
+++ b/services/wheelmap/wheelmap.tester.js
@@ -28,9 +28,9 @@ t.create('node with accessibility')
         })
       )
   )
-  .expectJSON({
-    name: 'accessibility',
-    value: 'yes',
+  .expectBadge({
+    label: 'accessibility',
+    message: 'yes',
     color: 'brightgreen',
   })
 
@@ -50,9 +50,9 @@ t.create('node with limited accessibility')
         })
       )
   )
-  .expectJSON({
-    name: 'accessibility',
-    value: 'limited',
+  .expectBadge({
+    label: 'accessibility',
+    message: 'limited',
     color: 'yellow',
   })
 
@@ -72,9 +72,9 @@ t.create('node without accessibility')
         })
       )
   )
-  .expectJSON({
-    name: 'accessibility',
-    value: 'no',
+  .expectBadge({
+    label: 'accessibility',
+    message: 'no',
     color: 'red',
   })
 
@@ -87,7 +87,7 @@ t.create('node not found')
       .get('/api/nodes/0')
       .reply(404)
   )
-  .expectJSON({
-    name: 'accessibility',
-    value: 'node not found',
+  .expectBadge({
+    label: 'accessibility',
+    message: 'node not found',
   })
diff --git a/services/wordpress/wordpress-downloads.tester.js b/services/wordpress/wordpress-downloads.tester.js
index fb224ebd17265e201a14e62099911194caeb9d56..7f31013d835d1cb7a5b4da6bdcf9eb76e438ce6d 100644
--- a/services/wordpress/wordpress-downloads.tester.js
+++ b/services/wordpress/wordpress-downloads.tester.js
@@ -12,108 +12,94 @@ module.exports = t
 
 t.create('Plugin Downloads - Total')
   .get('/plugin/dt/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 t.create('Plugin Downloads - Active')
   .get('/plugin/installs/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'active installs',
-      value: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]\+?$/),
-    })
-  )
+  .expectBadge({
+    label: 'active installs',
+    message: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]\+?$/),
+  })
 
 t.create('Plugin Downloads - Day')
   .get('/plugin/dd/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('Plugin Downloads - Week')
   .get('/plugin/dw/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('Plugin Downloads - Month')
   .get('/plugin/dm/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetricOverTimePeriod,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+  })
 
 t.create('Theme Downloads - Total')
   .get('/theme/dt/twentyseventeen.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'downloads',
-      value: isMetric,
-    })
-  )
+  .expectBadge({
+    label: 'downloads',
+    message: isMetric,
+  })
 t.create('Theme Downloads - Active')
   .get('/theme/installs/twentyseventeen.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'active installs',
-      value: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]\+?$/),
-    })
-  )
+  .expectBadge({
+    label: 'active installs',
+    message: Joi.string().regex(/^[1-9][0-9]*[kMGTPEZY]\+?$/),
+  })
 
 t.create('Plugin Downloads - Total | Not Found')
   .get('/plugin/dt/100.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'not found',
   })
 t.create('Plugin Downloads - Active | Not Found')
   .get('/plugin/installs/100.json')
-  .expectJSON({
-    name: 'active installs',
-    value: 'not found',
+  .expectBadge({
+    label: 'active installs',
+    message: 'not found',
   })
 
 t.create('Plugin Downloads - Day | Not Found')
   .get('/plugin/dd/100.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'plugin not found or too new',
+  .expectBadge({
+    label: 'downloads',
+    message: 'plugin not found or too new',
   })
 
 t.create('Plugin Downloads - Week | Not Found')
   .get('/plugin/dw/100.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'plugin not found or too new',
+  .expectBadge({
+    label: 'downloads',
+    message: 'plugin not found or too new',
   })
 
 t.create('Plugin Downloads - Month | Not Found')
   .get('/plugin/dm/100.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'plugin not found or too new',
+  .expectBadge({
+    label: 'downloads',
+    message: 'plugin not found or too new',
   })
 
 t.create('Theme Downloads - Total | Not Found')
   .get('/theme/dt/100.json')
-  .expectJSON({
-    name: 'downloads',
-    value: 'not found',
+  .expectBadge({
+    label: 'downloads',
+    message: 'not found',
   })
 t.create('Theme Downloads - Active | Not Found')
   .get('/theme/installs/100.json')
-  .expectJSON({
-    name: 'active installs',
-    value: 'not found',
+  .expectBadge({
+    label: 'active installs',
+    message: 'not found',
   })
diff --git a/services/wordpress/wordpress-platform.tester.js b/services/wordpress/wordpress-platform.tester.js
index b5d62ccacedb18858debde7042a6996f2a85da32..895e0ffc578791ea53898a2dc29ac513aaa9f094 100644
--- a/services/wordpress/wordpress-platform.tester.js
+++ b/services/wordpress/wordpress-platform.tester.js
@@ -11,30 +11,24 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Plugin Required WP Version')
   .get('/plugin/wp-version/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'wordpress',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'wordpress',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Plugin Tested WP Version')
   .get('/plugin/tested/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'wordpress',
-      value: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)? tested$/),
-    })
-  )
+  .expectBadge({
+    label: 'wordpress',
+    message: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)? tested$/),
+  })
 
 t.create('Plugin Tested WP Version (Alias)')
   .get('/v/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'wordpress',
-      value: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)? tested$/),
-    })
-  )
+  .expectBadge({
+    label: 'wordpress',
+    message: Joi.string().regex(/^v\d+(\.\d+)?(\.\d+)? tested$/),
+  })
 
 const mockedQuerySelector = {
   action: 'plugin_information',
@@ -72,9 +66,9 @@ t.create('Plugin Tested WP Version - current (mocked)')
       .get('/core/version-check/1.7/')
       .reply(200, mockedCoreResponseData)
   )
-  .expectJSON({
-    name: 'wordpress',
-    value: 'v4.9.8 tested',
+  .expectBadge({
+    label: 'wordpress',
+    message: 'v4.9.8 tested',
     color: 'brightgreen',
   })
 
@@ -96,9 +90,9 @@ t.create('Plugin Tested WP Version - old (mocked)')
       .get('/core/version-check/1.7/')
       .reply(200, mockedCoreResponseData)
   )
-  .expectJSON({
-    name: 'wordpress',
-    value: 'v4.9.6 tested',
+  .expectBadge({
+    label: 'wordpress',
+    message: 'v4.9.6 tested',
     color: 'orange',
   })
 
@@ -120,29 +114,29 @@ t.create('Plugin Tested WP Version - non-exsistant or unsupported (mocked)')
       .get('/core/version-check/1.7/')
       .reply(200, mockedCoreResponseData)
   )
-  .expectJSON({
-    name: 'wordpress',
-    value: 'v4.0.0 tested',
+  .expectBadge({
+    label: 'wordpress',
+    message: 'v4.0.0 tested',
     color: 'yellow',
   })
 
 t.create('Plugin Required WP Version | Not Found')
   .get('/plugin/wp-version/100.json')
-  .expectJSON({
-    name: 'wordpress',
-    value: 'not found',
+  .expectBadge({
+    label: 'wordpress',
+    message: 'not found',
   })
 
 t.create('Plugin Tested WP Version | Not Found')
   .get('/plugin/tested/100.json')
-  .expectJSON({
-    name: 'wordpress',
-    value: 'not found',
+  .expectBadge({
+    label: 'wordpress',
+    message: 'not found',
   })
 
 t.create('Plugin Tested WP Version (Alias) | Not Found')
   .get('/v/100.json')
-  .expectJSON({
-    name: 'wordpress',
-    value: 'not found',
+  .expectBadge({
+    label: 'wordpress',
+    message: 'not found',
   })
diff --git a/services/wordpress/wordpress-rating.tester.js b/services/wordpress/wordpress-rating.tester.js
index 675544c2712cff5cee1616c7557435f20f56ee9e..51eefdbac30cbb9b3680c527a2b29d027f253dba 100644
--- a/services/wordpress/wordpress-rating.tester.js
+++ b/services/wordpress/wordpress-rating.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isStarRating } = require('../test-validators')
 
@@ -12,64 +11,56 @@ module.exports = t
 
 t.create('Plugin Rating - Stars')
   .get('/plugin/stars/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
 
 t.create('Plugin Rating - Stars | Not Found')
   .get('/plugin/stars/100.json')
-  .expectJSON({
-    name: 'rating',
-    value: 'not found',
+  .expectBadge({
+    label: 'rating',
+    message: 'not found',
   })
 
 t.create('Plugin Rating - Stars (Alias)')
   .get('/plugin/r/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
 
 t.create('Plugin Rating - Stars (Alias) | Not Found')
   .get('/plugin/r/100.json')
-  .expectJSON({
-    name: 'rating',
-    value: 'not found',
+  .expectBadge({
+    label: 'rating',
+    message: 'not found',
   })
 
 t.create('Theme Rating - Stars')
   .get('/theme/stars/twentyseventeen.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
 
 t.create('Theme Rating - Stars | Not Found')
   .get('/theme/stars/100.json')
-  .expectJSON({
-    name: 'rating',
-    value: 'not found',
+  .expectBadge({
+    label: 'rating',
+    message: 'not found',
   })
 
 t.create('Theme Rating - Stars (Alias)')
   .get('/theme/r/twentyseventeen.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'rating',
-      value: isStarRating,
-    })
-  )
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
 
 t.create('Theme Rating - Stars (Alias) | Not Found')
   .get('/theme/r/100.json')
-  .expectJSON({
-    name: 'rating',
-    value: 'not found',
+  .expectBadge({
+    label: 'rating',
+    message: 'not found',
   })
diff --git a/services/wordpress/wordpress-version.tester.js b/services/wordpress/wordpress-version.tester.js
index a65a07d3ca4ebf4ab738c845829c9d10cc8efcc7..89409f9b4c465a4a6e60201a3924b75812bd7c6d 100644
--- a/services/wordpress/wordpress-version.tester.js
+++ b/services/wordpress/wordpress-version.tester.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const Joi = require('joi')
 const { ServiceTester } = require('../tester')
 const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
 
@@ -11,32 +10,28 @@ const t = (module.exports = new ServiceTester({
 
 t.create('Plugin Version')
   .get('/plugin/v/akismet.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'plugin',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'plugin',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Theme Version')
   .get('/theme/v/twentyseventeen.json')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'theme',
-      value: isVPlusDottedVersionAtLeastOne,
-    })
-  )
+  .expectBadge({
+    label: 'theme',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
 
 t.create('Plugin Version | Not Found')
   .get('/plugin/v/100.json')
-  .expectJSON({
-    name: 'plugin',
-    value: 'not found',
+  .expectBadge({
+    label: 'plugin',
+    message: 'not found',
   })
 
 t.create('Theme Version | Not Found')
   .get('/theme/v/100.json')
-  .expectJSON({
-    name: 'theme',
-    value: 'not found',
+  .expectBadge({
+    label: 'theme',
+    message: 'not found',
   })