Increase Testability using Test Doubles. Using the REST style to consume external services.
September 28th at 23.59
Go over the various test double implementations of SkyCave delegates/roles in the '...cave.doubles' package, and compare them to the Test Double terminology of Meszaros.
For each, discuss whether they indeed are a (stub, spy, fake object, mock, saboteur, ...) or not, that is, is the JavaDoc and class naming correct?
The 'getQuote()' method in PlayerServant is presently hardcoded to return a fixed string value, which of course does not allow us configure the daemon for either testing or for production usage. In this exercise, you will A) (TDD) develop a usable test double for the QuoteService interface B) refactor the PlayerServant and associated test code to invoke the configured quote service, and C) update client side test code to take the stub's indirect output values into account.
Requirements:
Hand-in:
Hints: I advice configuring the 'AllTestDoubleFactory' to use your new test double instead of the placeholder one I provided.
Evaluation:
I will use the grade sheet to evaluate your submission.
The test cases will be tested by Crunch.
Learning Goal | Assessment parameters |
Submission | The provided file names are full path source code file names that match the contents of the image that that Crunch downloads (So I can easily find your files!) |
Test Code | The test code is simple (no complex constructs, basically only assignments, simple private method calls, very basic for loop). The test code reflects using the TDD principles (Isolated Test, Evident Test, Evident Data, etc.). The production code is 'well' covered by test code (JaCoCo coverage is 'green' for most production code). |
Test Double Code | The test double code is clean, well named, and follows the principles that makes it a test double. The argumentation of which type of double is present and correct. |
PlayerServant Code | PlayerServant's 'getQuote()' and other code is correctly updated to use the QuoteService interface, and use the dependency injection system through CPF's to bind to the proper test double (in the 'http.cpf' configuration). |
Each individual student must register in the SkyCave
SubscriptionService to establish login credentials. Please
visit the web site https://cavereg (dot) baerbak (dot)
com (colon) 7654
, and enter your login name/student id,
define a player name and password, and enter your group name.
If you browser just shows rubbish, it is because you have forgotten to enter https://.
Important! All group members must enter exactly the same group name, respecting case. If you do not, then you will not be able to hand-in your code base for Crunch processing nor get the points for solving exercises!
In the next exercise, your SkyCave will use an authorization API to switch to 'single sign-on' using this service in order to allow all students to enter everybody else's cave.
Please: Do not post the URL of the cave registration publicly, it is our little secret. We do not want half the world to register!
The SubscriptionService is implemented by a Fake Object test double in the delivered Skycave daemon code. We need to contact the real SubscriptionService.
Create a new
implementation that contacts the real SkyCave subscription
server at server address: https://cavereg (dot) baerbak (dot)
com (colon) 7654
, and path
/api/v3/authorize
The authorize protocol is a REST call that 'mimics' OAuth 2.0 (The two steps of /authorize and /token calls are merge into a single one) using the protocol outlined as below, using the syntax in 'Flexible, Reliable, Distributed Software' §7.7:
Subscription Service ==================== Authorize a loginName ---- POST /api/v3/authorize Accept: application/json Authorization: Basic skycave_daemon:{daemon_pwd} { "loginName": {login name} "password": {password} } Response: Status: 401 UNAUTHORIZED { "httpStatusCode": 401, "message": (error description) } Status: 500 INTERNAL SERVER ERROR { "httpStatusCode": 500, "message": (error description) } Status: 200 OK { "accessToken": "4ccb811e-df79-4385-a1ac-f3f2df647234", "httpStatusCode": 200, "message": "loginName (name) was authorized", "subscription": { "dateCreated": "2015-06-14 13:01 PM CEST", "groupName": "group-10", "groupToken": "Manganese946_Serbia419", "loginName": "name", "playerID": "a3607675-99b4-4ab7-8aa9-6f592676227c", "playerName": "EliaJørg", "region": "AALBORG" } } Comment: This POST request merges the OAuth 2's protocol of GET /authorize and POST /token in a single request; however the response carries a full SubscriptionRecord PODO as JSON. The OAuth relevant 'access token' is the value of key 'accessToken' in returned JSON. Returned status codes are the standard HTTP 401 / UNAUTHORIZED and 200 / OK status codes, and is also replicated in the JSON payload. 500 may be returned in case of server malfunction, typically internal persistent storage failure. The basic authentication is standard HTTP Base64 encoded of the literal "skycave_daemon" and the password for the daemon, separated by colon.
Your SkyCave daemon must identify itself in the Basic Authorization section of the POST request. The username is 'skycave_daemon' and the password is 'f8tqv56utx'.
You can manually test the API, if you install 'httpie', and issue a POST, similar to the screenshot below:
Ensure that hostname and port is configured through the value of properties SKYCAVE_SUBSCRIPTIONSERVICE_SERVER_ADDRESS and that the class name of your Java implementation of the SubscriptionService interface is defined by the SKYCAVE_SUBSCRIPTIONSERVICE_CONNECTOR_IMPLEMENTATION in your 'subscription-service.cpf' file, not through hardcoded links in your source code!
To implement it, find some suitable java library candidates for REST/HTTP communication, available from maven repository, (or use the Java11 built in one) and test them out before deciding. Do not pick 'the first the best', pick one that comes with an expressive API, so you don't have to write a lot of boilerplate coding. Take small steps! Detailed record keeping!
Again, please keep the URL as our secret to avoid web crawlers and other pests.
Requirements:
subscription-service.cpf
in your server gradle module based upon
'http.cpf' defining the following configuration (**Real**
shows the change from the one in http.cpf):
Subscription: **Real** | CaveStorage: Fake | Quote: TestDouble | PlayerNameService: InMemory |
Hand-in: Add the 'subscription-service' exercise to your group's list in Crunch (See the tools page on how to do that.)
Evaluation: Execution by Crunch.
Hint: In preparation for later exercises, ensure that the HTTP library you use supports setting timeouts on connections! (And that it actually works - sometimes API docs are a bit over-optimistic :)
Hint 2: All hell can of course break loose, once you have
remote communication (network errors, server offline, ...).
Ignore it for now - catch the exceptions and emit
logger.error(...)'
stuff. We will return to 'design for
failure' in the next course.
The quote service is currently just implemented as a test stub
in your 'daemon'. Enhance it so it contacts the real SkyCave
quote service at the server: quote
(dot) baerbak (dot) com (colon) 6777
. (Again, the
'moja.st.client.au.dk' was used in the examples below, but
should be 'quote.baerbak.com'.)
The service responds only on two paths as outlined in the following API using the format described in FRDS §7.7.
GET quote header ---------------- GET /msdo/v1/quotes (none) Response Status: 200 OK { "authors": [ "Albert Einstein", "Søren Kierkegaard", ], "published": "2019-06-28T09:35:19.133Z", "title": "MSDO Quote Service", "totalItems": 57, "url": "http://moja.st.client.au.dk:6777/msdo/v1/quotes" } Status: 400 BAD REQUEST 400 is returned in case the path is not well formed.
GET individual quote ------------------- GET /msdo/v1/quotes/{quoteIndex} Response Status: 200 OK { "author": "Albert Einstein", "number": 1, "quote": "Logic will get you from A to B. Imagination will take you everywhere." } Status: 404 NOT FOUND Status: 400 BAD REQUEST 404 is returned in case the quoteIndex is out of range. 400 is returned in case the quoteIndex is not well formed (not integer) or the path does not match any of the above two. First valid quoteIndex is 1.
You can contact the server yourself using 'curl' or 'http' from the commandline:
Requirements:
quote-service.cpf
whose
properties SKYCAVE_QUOTESERVICE_CONNECTOR_IMPLEMENTATION
and SKYCAVE_QUOTESERVICE_SERVER_ADDRESS reflect your
implementation and integration with the official quote
server. The configuration must be:
Subscription: Fake | CaveStorage: Fake | Quote: **Real** | PlayerNameService: InMemory |
Hand-in: Add the exercise to your group's list in Crunch.
Evaluation: Crunch.
Hint: Again - just log connection failures, etc.
Production Checkpoint! Update your production environment, so your production server in the cloud uses the Subscription service as well as the Quote service.
(And do not make Crunch evaluate it until after you have been rewarded points for the 'operations' exercise! Crunch will for obvious reasons only try to evaluate the most advanced of the 'operations' exercises. You cannot maintain a production environment in multiple variants.)
Requirements:
operations.cpf
CPF file in the client subproject, and keep doing its
test journey tests.
Hand-in: Submit to Crunch.
Evaluation: Crunch will login using credentials from the subscription service, and fetch a few quotes.
If you issue command 'quote 0' in the 'cmd' then you should get a random quote from the full set of quotes in the quote server.
To compute that, you need to retrieve the quote header from the QuoteService, extract the maximum number of quotes in the service (let us call it 'max') and then compute a random integer value in the interval 1..max.
Requirements:
Learning objectives: Creating a saboteur quote-service using mountebank.
Note: This is pretty much outside the scope of the current course, but Mountebank is a nice tool that I find is valuable to know when we try to test Nygard 'Safe Failure Modes'...
Create a Mountebank test double service which responds to the three paths:
/msdo/v1/quotes /msdo/v1/quotes/1 /msdo/v1/quotes/7 /msdo/v1/quotes/13and returns similar JSON responses as does the Java Test stub (quotes by Einstein, and by Bærbak).
Any other path requests should return 404 NOT FOUND replies.
The Einstein response should have a delay by 500ms while the Bærbak response should delay 3000ms.
Test it by having a 'moutebank-quote-service.cpf' that points to your running mountebank service.
Hints: I managed to solve this by a one-liner 'docker run' command by using a) volume mounting and b) '- - configfile' option to mountebank.