diff --git a/404.html b/404.html index e48dafca..1ecabaae 100644 --- a/404.html +++ b/404.html @@ -9,13 +9,13 @@ - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/js/93571253.4d0f9174.js b/assets/js/93571253.4d0f9174.js deleted file mode 100644 index d93fe875..00000000 --- a/assets/js/93571253.4d0f9174.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1265],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},u=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||h[p]||o;return n?a.createElement(m,r(r({ref:t},u),{},{components:n})):a.createElement(m,r({ref:t},u))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:19.875,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},nextItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],u={toc:c};function h(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},u,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects")),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"))}h.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/93571253.ab9737d3.js b/assets/js/93571253.ab9737d3.js new file mode 100644 index 00000000..7f4fb7ab --- /dev/null +++ b/assets/js/93571253.ab9737d3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1265],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},u=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||h[p]||o;return n?a.createElement(m,r(r({ref:t},u),{},{components:n})):a.createElement(m,r({ref:t},u))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:19.875,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},nextItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],u={toc:c};function h(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},u,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects")),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"))}h.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/b2f554cd.b6270e72.js b/assets/js/b2f554cd.b6270e72.js deleted file mode 100644 index 06f26849..00000000 --- a/assets/js/b2f554cd.b6270e72.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1477],{10:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"testing-the-dark-scenarios-of-your-nodejs-application","metadata":{"permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md","source":"@site/blog/crucial-tests/index.md","title":"Testing the dark scenarios of your Node.js application","description":"Where the dead-bodies are covered","date":"2023-07-07T11:00:00.000Z","formattedDate":"July 7, 2023","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"testing","permalink":"/blog/tags/testing"},{"label":"component-test","permalink":"/blog/tags/component-test"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"unit-test","permalink":"/blog/tags/unit-test"},{"label":"integration","permalink":"/blog/tags/integration"},{"label":"nock","permalink":"/blog/tags/nock"}],"readingTime":19.875,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"}],"frontMatter":{"slug":"testing-the-dark-scenarios-of-your-nodejs-application","date":"2023-07-07T11:00","hide_table_of_contents":true,"title":"Testing the dark scenarios of your Node.js application","authors":["goldbergyoni","razluvaton"],"tags":["node.js","testing","component-test","fastify","unit-test","integration","nock"]},"nextItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"}},"content":"## Where the dead-bodies are covered\\n\\nThis post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked\\n\\nSome context first: How do we test a modern backend? With [the testing diamond](https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55), of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we\'ve also written [a guide with 50 best practices for integration tests in Node.js](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n\\nBut there is a pitfall: most developers write _only_ semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don\'t simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime\\n\\n![The hidden corners](./the-hidden-corners.png)\\n\\nHere are a handful of examples that might open your mind to a whole new class of risks and tests\\n\\n## \ud83e\udddf\u200d\u2640\ufe0f The zombie process test\\n\\n**\ud83d\udc49What & so what? -** In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see [readiness probe](https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod.)). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a \'zombie process\'. In this scenario, the runtime platform won\'t realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, api.js:**\\n\\n```javascript\\n// A common express server initialization\\nconst startWebServer = () => {\\n return new Promise((resolve, reject) => {\\n try {\\n // A typical Express setup\\n expressApp = express();\\n defineRoutes(expressApp); // a function that defines all routes\\n expressApp.listen(process.env.WEB_SERVER_PORT);\\n } catch (error) {\\n //log here, fire a metric, maybe even retry and finally:\\n process.exit();\\n }\\n });\\n};\\n```\\n\\n**The test:**\\n\\n```javascript\\nconst api = require(\'./entry-points/api\'); // our api starter that exposes \'startWebServer\' function\\nconst sinon = require(\'sinon\'); // a mocking library\\n\\ntest(\'When an error happens during the startup phase, then the process exits\', async () => {\\n // Arrange\\n const processExitListener = sinon.stub(process, \'exit\');\\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\\n sinon\\n .stub(routes, \'defineRoutes\')\\n .throws(new Error(\'Cant initialize connection\'));\\n\\n // Act\\n await api.startWebServer();\\n\\n // Assert\\n expect(processExitListener.called).toBe(true);\\n});\\n```\\n\\n## \ud83d\udc40 The observability test\\n\\n**\ud83d\udc49What & why -** For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error **correctly observable**. In plain words, ensuring that it\'s being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, _including stack trace_, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn\'t care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When exception is throw during request, Then logger reports the mandatory fields\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n status: \'approved\',\\n };\\n const metricsExporterDouble = sinon.stub(metricsExporter, \'fireMetric\');\\n sinon\\n .stub(OrderRepository.prototype, \'addOrder\')\\n .rejects(new AppError(\'saving-failed\', \'Order could not be saved\', 500));\\n const loggerDouble = sinon.stub(logger, \'error\');\\n\\n //Act\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n expect(loggerDouble).toHaveBeenCalledWith({\\n name: \'saving-failed\',\\n status: 500,\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n expect(\\n metricsExporterDouble).toHaveBeenCalledWith(\'error\', {\\n errorName: \'example-error\',\\n })\\n});\\n```\\n\\n## \ud83d\udc7d The \'unexpected visitor\' test - when an uncaught exception meets our code\\n\\n**\ud83d\udc49What & why -** A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let\'s focus on the 2nd assumption: it\'s common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on(\'error\', ...). To name a few examples. These errors will find their way to the global process.on(\'uncaughtException\') handler, **hopefully if your code subscribed**. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here\'s a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is \'borderless\', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here\'s an example:\\n\\nresearches says that, rejection\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When an unhandled exception is thrown, then process stays alive and the error is logged\', async () => {\\n //Arrange\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const processExitListener = sinon.stub(process, \'exit\');\\n const errorToThrow = new Error(\'An error that wont be caught \ud83d\ude33\');\\n\\n //Act\\n process.emit(\'uncaughtException\', errorToThrow); //\ud83d\udc48 Where the magic is\\n\\n // Assert\\n expect(processExitListener.called).toBe(false);\\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\\n});\\n```\\n\\n## \ud83d\udd75\ud83c\udffc The \'hidden effect\' test - when the code should not mutate at all\\n\\n**\ud83d\udc49What & so what -** In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn\'t have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn\'t guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you\'re not cleaning the DB often (like me, but that\'s another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\nit(\'When adding an invalid order, then it returns 400 and NOT retrievable\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n mode: \'draft\',\\n externalIdentifier: uuid(), //no existing record has this value\\n };\\n\\n //Act\\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\\n \'/order\',\\n orderToAdd\\n );\\n\\n //Assert\\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\\n ); // Trying to get the order that should have failed\\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\\n addingHTTPStatus: 400,\\n fetchingHTTPStatus: 404,\\n });\\n // \ud83d\udc46 Check that no such record exists\\n});\\n```\\n\\n## \ud83e\udde8 The \'overdoing\' test - when the code should mutate but it\'s doing too much\\n\\n**\ud83d\udc49What & why -** This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here\'s a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more \'control\' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When deleting an existing order, Then it should NOT be retrievable\', async () => {\\n // Arrange\\n const orderToDelete = {\\n userId: 1,\\n productId: 2,\\n };\\n const deletedOrder = (await axiosAPIClient.post(\'/order\', orderToDelete)).data\\n .id; // We will delete this soon\\n const orderNotToBeDeleted = orderToDelete;\\n const notDeletedOrder = (\\n await axiosAPIClient.post(\'/order\', orderNotToBeDeleted)\\n ).data.id; // We will not delete this\\n\\n // Act\\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\\n\\n // Assert\\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${deletedOrder}`\\n );\\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${notDeletedOrder}`\\n );\\n expect(getNotDeletedOrderStatus).toBe(200);\\n expect(getDeletedOrderStatus).toBe(404);\\n});\\n```\\n\\n## \ud83d\udd70 The \'slow collaborator\' test - when the other HTTP service times out\\n\\n**\ud83d\udc49What & why -** When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it\'s harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like [nock](https://github.com/nock/nock) or [wiremock](https://wiremock.org/). These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available **in production**, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can\'t wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other \'chaotic\' scenarios. Question left is how to simulate slow response without having slow tests? You may use [fake timers](https://sinonjs.org/releases/latest/fake-timers/) and trick the system into believing as few seconds passed in a single tick. If you\'re using [nock](https://github.com/nock/nock), it offers an interesting feature to simulate timeouts **quickly**: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\n// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\\ntest(\'When users service times out, then return 503 (option 1 with fake timers)\', async () => {\\n //Arrange\\n const clock = sinon.useFakeTimers();\\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\\n nock(`${config.userServiceURL}/user/`)\\n .get(\'/1\', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\\n .reply(200);\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n mode: \'approved\',\\n };\\n\\n //Act\\n // \ud83d\udc47try to add new order which should fail due to User service not available\\n const response = await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n // \ud83d\udc47At least our code does its best given this situation\\n expect(response.status).toBe(503);\\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\\n name: \'user-service-not-available\',\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n});\\n```\\n\\n## \ud83d\udc8a The \'poisoned message\' test - when the message consumer gets an invalid payload that might put it in stagnation\\n\\n**\ud83d\udc49What & so what -** When testing flows that start or end in a queue, I bet you\'re going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you\'re using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the \'poisoned message\'. To mitigate this risk, the tests\' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why\\n\\nWhen testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. [SQS demand 60 seconds](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html) to purge queues), to name a few challenges that you won\'t find when dealing with real DB\\n\\nHere is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By \'fake\' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like [this one for SQS](https://github.com/m-radzikowski/aws-sdk-client-mock) and you can code one **easily** yourself. No worries, I\'m not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):\\n\\n**\ud83d\udcdd Code**\\n\\n1. Create a fake message queue that does almost nothing but record calls, see full example here\\n\\n```javascript\\nclass FakeMessageQueueProvider extends EventEmitter {\\n // Implement here\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n}\\n```\\n\\n2. Make your message queue client accept real or fake provider\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n // Pass to it a fake or real message queue\\n constructor(customMessageQueueProvider) {}\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // Simple implementation can be found here:\\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\\n}\\n```\\n\\n3. Expose a convenient function that tells when certain calls where made\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // \ud83d\udc47\\n waitForEvent(eventName: \'publish\' | \'consume\' | \'acknowledge\' | \'reject\', howManyTimes: number) : Promise\\n}\\n```\\n\\n4. The test is now short, flat and expressive \ud83d\udc47\\n\\n```javascript\\nconst FakeMessageQueueProvider = require(\'./libs/fake-message-queue-provider\');\\nconst MessageQueueClient = require(\'./libs/message-queue-client\');\\nconst newOrderService = require(\'./domain/newOrderService\');\\n\\ntest(\'When a poisoned message arrives, then it is being rejected back\', async () => {\\n // Arrange\\n const messageWithInvalidSchema = { nonExistingProperty: \'invalid\u274c\' };\\n const messageQueueClient = new MessageQueueClient(\\n new FakeMessageQueueProvider()\\n );\\n // Subscribe to new messages and passing the handler function\\n messageQueueClient.consume(\'orders.new\', newOrderService.addOrder);\\n\\n // Act\\n await messageQueueClient.publish(\'orders.new\', messageWithInvalidSchema);\\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\\n\\n // Assert\\n await messageQueueClient.waitFor(\'reject\', { howManyTimes: 1 });\\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js)\\n\\n## \ud83d\udce6 Test the package as a consumer\\n\\n**\ud83d\udc49What & why -** When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user\'s computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts _that were built_. See the mismatch here? _after_ running the tests, the package files are transpiled (I\'m looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files\\n\\n**\ud83d\udcdd Code**\\n\\nConsider the following scenario, you\'re developing a library, and you wrote this code:\\n```js\\n// index.js\\nexport * from \'./calculate.js\';\\n\\n// calculate.js \ud83d\udc48\\nexport function calculate() {\\n return 1;\\n}\\n```\\n\\nThen some tests:\\n```js\\nimport { calculate } from \'./index.js\';\\n\\ntest(\'should return 1\', () => {\\n expect(calculate()).toBe(1);\\n})\\n\\n\u2705 All tests pass \ud83c\udf8a\\n```\\n\\nFinally configure the package.json:\\n```json5\\n{\\n // ....\\n \\"files\\": [\\n \\"index.js\\"\\n ]\\n}\\n```\\n\\nSee, 100% coverage, all tests pass locally and in the CI \u2705, it just won\'t work in production \ud83d\udc79. Why? because you forgot to include the `calculate.js` in the package.json `files` array \ud83d\udc46\\n\\n\\nWhat can we do instead? we can test the library as _its end-users_. How? publish the package to a local registry like [verdaccio](https://verdaccio.org/), let the tests install and approach the *published* code. Sounds troublesome? judge yourself \ud83d\udc47\\n\\n**\ud83d\udcdd Code**\\n\\n```js\\n// global-setup.js\\n\\n// 1. Setup the in-memory NPM registry, one function that\'s it! \ud83d\udd25\\nawait setupVerdaccio();\\n\\n// 2. Building our package \\nawait exec(\'npm\', [\'run\', \'build\'], {\\n cwd: packagePath,\\n});\\n\\n// 3. Publish it to the in-memory registry\\nawait exec(\'npm\', [\'publish\', \'--registry=http://localhost:4873\'], {\\n cwd: packagePath,\\n});\\n\\n// 4. Installing it in the consumer directory\\nawait exec(\'npm\', [\'install\', \'my-package\', \'--registry=http://localhost:4873\'], {\\n cwd: consumerPath,\\n});\\n\\n// Test file in the consumerPath\\n\\n// 5. Test the package \ud83d\ude80\\ntest(\\"should succeed\\", async () => {\\n const { fn1 } = await import(\'my-package\');\\n\\n expect(fn1()).toEqual(1);\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/rluvaton/e2e-verdaccio-example)\\n\\nWhat else this technique can be useful for?\\n\\n- Testing different version of peer dependency you support - let\'s say your package support react 16 to 18, you can now test that\\n- You want to test ESM and CJS consumers\\n- If you have CLI application you can test it like your users\\n- Making sure all the voodoo magic in that babel file is working as expected\\n\\n## \ud83d\uddde The \'broken contract\' test - when the code is great but its corresponding OpenAPI docs leads to a production bug\\n\\n**\ud83d\udc49What & so what -** Quite confidently I\'m sure that almost no team test their OpenAPI correctness. \\"It\'s just documentation\\", \\"we generate it automatically based on code\\" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.\\n\\nConsider the following scenario, you\'re requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don\'t forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the \'contract\' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., [PACT](https://pact.io)), there are also leaner approaches that gets you covered _easily and quickly_ (at the price of covering less risks).\\n\\nThe following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It\'s a pity that these libs can\'t assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, an API throw a new error status**\\n\\n```javascript\\nif (doesOrderCouponAlreadyExist) {\\n throw new AppError(\'duplicated-coupon\', { httpStatus: 409 });\\n}\\n```\\n\\nThe OpenAPI doesn\'t document HTTP status \'409\', no framework knows to update the OpenAPI doc based on thrown exceptions\\n\\n```json\\n\\"responses\\": {\\n \\"200\\": {\\n \\"description\\": \\"successful\\",\\n }\\n ,\\n \\"400\\": {\\n \\"description\\": \\"Invalid ID\\",\\n \\"content\\": {}\\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\\n}\\n\\n```\\n\\n**The test code**\\n\\n```javascript\\nconst jestOpenAPI = require(\'jest-openapi\');\\njestOpenAPI(\'../openapi.json\');\\n\\ntest(\'When an order with duplicated coupon is added , then 409 error should get returned\', async () => {\\n // Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n couponId: uuid(),\\n };\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n // Act\\n // We\'re adding the same coupon twice \ud83d\udc47\\n const receivedResponse = await axios.post(\'/order\', orderToAdd);\\n\\n // Assert;\\n expect(receivedResponse.status).toBe(409);\\n expect(res).toSatisfyApiSpec();\\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\\n});\\n```\\n\\nTrick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in \'beforeAll\'. This covers all the tests against OpenAPI mismatches\\n\\n```javascript\\nbeforeAll(() => {\\n axios.interceptors.response.use((response) => {\\n expect(response.toSatisfyApiSpec());\\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\\n });\\n});\\n```\\n\\n## Even more ideas\\n\\n- Test readiness and health routes\\n- Test message queue connection failures\\n- Test JWT and JWKS failures\\n- Test security-related things like CSRF tokens\\n- Test your HTTP client retry mechanism (very easy with nock)\\n- Test that the DB migration succeed and the new code can work with old records format\\n- Test DB connection disconnects\\n \\n## It\'s not just ideas, it a whole new mindset\\n\\nThe examples above were not meant only to be a checklist of \'don\'t forget\' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this \'production-oriented development\'"},{"id":"monorepo-backend","metadata":{"permalink":"/blog/monorepo-backend","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md","source":"@site/blog/which-monorepo/index.md","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","description":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we\'d like to share our considerations in choosing our monorepo tooling","date":"2023-07-07T05:31:27.000Z","formattedDate":"July 7, 2023","tags":[{"label":"monorepo","permalink":"/blog/tags/monorepo"},{"label":"decisions","permalink":"/blog/tags/decisions"}],"readingTime":16.925,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"monorepo-backend","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","authors":["goldbergyoni","michaelsalomon"],"tags":["monorepo","decisions"]},"prevItem":{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},"nextItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"}},"content":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in [Practica.js](https://github.com/practicajs/practica). In this post, we\'d like to share our considerations in choosing our monorepo tooling\\n\\n![Monorepos](./monorepo-high-level.png)\\n\\n## What are we looking\xa0at\\n\\n\\nThe Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 [Lerna- has just retired.](https://github.com/lerna/lerna/issues/2703) When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.\\n\\nThis post is concerned with backend-only and Node.js. It also scoped to _typical_ business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like [Bazel](https://github.com/thundergolfer/example-bazel-monorepo) is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison).\\n\\nLet\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following _4 layers below._ Each one of them can bring value to your project, each has different consequences, tooling, and features:\\n\\n\x3c!--truncate--\x3e\\n\\n\\n# Layer 1: Plain old folders to stay on top of your code\\n\\nWith zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, _quickly_ adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.\\n\\nThis layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling.\\n\\nWith that said, some of the newer tools augment this experience with interesting features:\\n\\n- Both [Turborepo](https://turborepo.org/) and [Nx](https://nx.dev/structure/dependency-graph) and also [Lerna](https://www.npmjs.com/package/lerna-dependency-graph) provide a visual representation of the packages\u2019 dependencies\\n- [Nx allows \u2018visibility rules\u2019](https://nx.dev/structure/monorepo-tags) which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)\\n\\n![](https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg)\\n\\nNx dependencies graph\\n\\n- [Nx workspace generator](https://nx.dev/generators/workspace-generators) allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing\\n\\n# Layer 2: Tasks and pipeline to build your code efficiently\\n\\nEven in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of _multiple_ components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.\\n\\n![](https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png)\\n\\nApply some commands over multiple packages\\n\\nIn some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 _deeply_ optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:\\n\\n- **Parallelization \u2014** If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?\\n- **Smart execution plan \u2014**Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s _isolated_ unit tests _while_ building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement\\n\\n![](https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png)\\n\\nA modern tool advantage over old Lerna. Taken from Turborepo website\\n\\n- **Detect who is affected by a change \u2014** Even on a system with high coupling between packages, it\u2019s usually not necessary to run _all_ packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed\\n- **Sub-systems (i.e., projects) \u2014** Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group\\n- **Caching \u2014** This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!\\n- **Remote caching \u2014** Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time\\n\\n# Layer 3: Hoist your dependencies to boost npm installation\\n\\nThe speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate ([the NPM doppelgangers problem](https://rushjs.io/pages/advanced/npm_doppelgangers/)) the installation of those packages which might result in a long process.\\n\\nThis is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.\\n\\nBoth Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.\\n\\n![](https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png)\\n\\nOn top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default.\\n\\nI\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js _runtime_ and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019.\\n\\n# Stop for a second: It\u2019s all about your workflow\\n\\nWe deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is _synchronized or independent_ (we will discuss this in a few seconds)_._ This upfront _fundamental_ decision will change almost everything.\\n\\nConsider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?\\n\\n**Option A \u2014 The synchronized workflow-** Going with this development style, all the three components will be developed and deployed in one chunk _together_. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they\'re ready, the version of all components will get bumped. Finally, they will get deployed _together._\\n\\nGoing with this approach, the developer has the chance of seeing the full flow from the client\'s perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.\\n\\n**Option B \u2014 Independent workflow-** This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it.\\n\\nGoing with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also _forces her_ to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious.\\n\\n![](https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png)\\n\\nSynchronized and independent workflows illustrated\\n\\n**On the illusion of synchronicity**\\n\\nIn distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.\\n\\nThis fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph.\\n\\n# Layer 4: Link your packages for immediate feedback\\n\\nWhen having a Monorepo, there is always the unavoidable dilemma of how to link between the components:\\n\\n**Option 1: Using npm \u2014** Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.\\n\\n**Option2: Just a plain folder \u2014** With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented.\\n\\nWith option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes.\\n\\nWith option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.\\n\\nHow do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry.\\n\\n![](https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png)\\n\\nLinking packages in a Monorepo\\n\\nIf you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.\\n\\nIf favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is _temporarily link_ (with npm link) between the packages_,_ debug, code, then finally remove the link.\\n\\nNx is taking a slightly more disruptive approach \u2014 it is using [TypeScript paths](https://www.typescriptlang.org/tsconfig#paths) to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.\\n\\n# Closing: What should you use?\\n\\nIt\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision.\\n\\n**Scenario A \u2014** If your architecture dictates a _synchronized workflow_ where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.\\n\\nFor example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many _relatively_ coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.\\n\\nIf your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.\\n\\n![](https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png)\\n\\n**Scenario B\u2014** If you\u2019re into an _independent workflow_ where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is [Bilt](https://github.com/giltayar/bilt) by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.\\n\\n**In any scenario, consider workspaces \u2014** If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain.\\n\\nWe tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.\\n\\n# Bonus: Comparison table\\n\\nSee below a detailed comparison table of the various tools and features:\\n\\n![](https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png)\\n\\nPreview only, the complete table can be [found here](https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md)"},{"id":"practica-v0.0.6-is-alive","metadata":{"permalink":"/blog/practica-v0.0.6-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md","source":"@site/blog/v0.6-is-alive/index.md","title":"Practica v0.0.6 is alive","description":"Where is our focus now?","date":"2022-12-10T10:00:00.000Z","formattedDate":"December 10, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"prisma","permalink":"/blog/tags/prisma"}],"readingTime":1.47,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"},{"name":"Daniel Gluskin","title":"Practica.js core maintainer","url":"https://github.com/DanielGluskin","imageURL":"https://avatars.githubusercontent.com/u/17989958?v=4","key":"danielgluskin"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"practica-v0.0.6-is-alive","date":"2022-12-10T10:00","hide_table_of_contents":true,"title":"Practica v0.0.6 is alive","authors":["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],"tags":["node.js","express","practica","prisma"]},"prevItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"},"nextItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"}},"content":"## Where is our focus now?\\n\\nWe work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback\\n\\n## What\'s new?\\n\\n### Request-level store\\n\\nEvery request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is \'request-id\' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in [AsyncLocal](https://nodejs.org/api/async_context.html) for this task\\n\\n### Hardened .dockerfile\\n\\nAlthough a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from [this article](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/) and already apply 90% of the guidelines\\n\\n### Additional ORM option: Prisma\\n\\nPrisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma\\n\\nWhy did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this [blog post](https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/)\\n\\n### Many small enhancements\\n\\nMore than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more\\n\\n## Where do I start?\\n\\nDefinitely follow the [getting started guide first](https://practica.dev/the-basics/getting-started-quickly) and then read the guide [coding with practica](https://practica.dev/the-basics/coding-with-practica) to realize its full power and genuine value. We will be thankful to receive your feedback"},{"id":"is-prisma-better-than-your-traditional-orm","metadata":{"permalink":"/blog/is-prisma-better-than-your-traditional-orm","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/is-prisma-better/index.md","source":"@site/blog/is-prisma-better/index.md","title":"Is Prisma better than your \'traditional\' ORM?","description":"Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?","date":"2022-12-07T11:00:00.000Z","formattedDate":"December 7, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":23.875,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"is-prisma-better-than-your-traditional-orm","date":"2022-12-07T11:00","hide_table_of_contents":true,"title":"Is Prisma better than your \'traditional\' ORM?","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"},"nextItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"## Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?\\n\\n*Betteridge\'s law of headlines suggests that a \'headline that ends in a question mark can be answered by the word NO\'. Will this article follow this rule?*\\n\\nImagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it\'s hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained\\n\\n![Suite with stain](./suite.png)\\n\\n Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, \\"I wish we had something like (Java) hibernate or (.NET) Entity Framework\\" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don\'t feel delightful, some may say even mediocre. At least so I believed *before* writing this article...\\n\\nFrom time to time, a shiny new ORM is launched, and there is hope. Then soon it\'s realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It\'s gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the \'Ferrari\' ORM we\'ve been waiting for? Is it a game changer? If you\'re are the \'no ORM for me\' type, will this one make you convert your religion?\\n\\nIn [Practica.js](https://github.com/practicajs/practica) (the Node.js starter based off [Node.js best practices with 83,000 stars](https://github.com/goldbergyoni/nodebestpractices)) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?\\n\\nThis article is certainly not an \'ORM 101\' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It\'s compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren\'t covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs\\n\\nReady to explore how good Prisma is and whether you should throw away your current tools?\\n\\n\x3c!--truncate--\x3e\\n\\n## TOC\\n\\n1. Prisma basics in 3 minutes\\n2. Things that are mostly the same\\n3. Differentiation\\n4. Closing\\n\\n## Prisma basics in 3 minutes\\n\\nJust before delving into the strategic differences, for the benefit of those unfamiliar with Prisma - here is a quick \'hello-world\' workflow with Prisma ORM. If you\'re already familiar with it - skipping to the next section sounds sensible. Simply put, Prisma dictates 3 key steps to get our ORM code working:\\n\\n**A. Define a model -** Unlike almost any other ORM, Prisma brings a unique language (DSL) for modeling the database-to-code mapping. This proprietary syntax aims to express these models with minimum clutter (i.e., TypeScript generics and verbose code). Worried about having intellisense and validation? A well-crafted vscode extension gets you covered. In the following example, the prisma.schema file describes a DB with an Order table that has a one-to-many relation with a Country table:\\n\\n```prisma\\n// prisma.schema file\\nmodel Order {\\n id Int @id @default(autoincrement())\\n userId Int?\\n paymentTermsInDays Int?\\n deliveryAddress String? @db.VarChar(255)\\n country Country @relation(fields: [countryId], references: [id])\\n countryId Int\\n}\\n\\nmodel Country {\\n id Int @id @default(autoincrement())\\n name String @db.VarChar(255)\\n Order Order[]\\n}\\n```\\n\\n**B. Generate the client code -** Another unusual technique: to get the ORM code ready, one must invoke Prisma\'s CLI and ask for it: \\n\\n```bash\\nnpx prisma generate\\n```\\n\\nAlternatively, if you wish to have your DB ready and the code generated with one command, just fire:\\n\\n```bash\\nnpx prisma migrate deploy\\n```\\n\\nThis will generate migration files that you can execute later in production and also the ORM client code\\n\\n\\nThis will generate migration files that you can execute later in production and the TypeScript ORM code based on the model. The generated code location is defaulted under \'[root]/NODE_MODULES/.prisma/client\'. Every time the model changes, the code must get re-generated again. While most ORMs name this code \'repository\' or \'entity\' or \'active record\', interestingly, Prisma calls it a \'client\'. This shows part of its unique philosophy, which we will explore later\\n\\n**C. All good, use the client to interact with the DB -** The generated client has a rich set of functions and types for your DB interactions. Just import the ORM/client code and use it:\\n\\n```javascript\\nimport { PrismaClient } from \'.prisma/client\';\\n\\nconst prisma = new PrismaClient();\\n// A query example\\nawait prisma.order.findMany({\\n where: {\\n paymentTermsInDays: 30,\\n },\\n orderBy: {\\n id: \'asc\',\\n },\\n });\\n// Use the same client for insertion, deletion, updates, etc\\n```\\n\\nThat\'s the nuts and bolts of Prisma. Is it different and better?\\n\\n## What is the same?\\n\\nWhen comparing options, before outlining differences, it\'s useful to state what is actually similar among these products. Here is a partial list of features that both TypeORM, Sequelize and Prisma support\\n\\n- Casual queries with sorting, filtering, distinct, group by, \'upsert\' (update or create),etc\\n- Raw queries\\n- Full text search\\n- Association/relations of any type (e.g., many to many, self-relation, etc)\\n- Aggregation queries\\n- Pagination\\n- CLI\\n- Transactions\\n- Migration & seeding\\n- Hooks/events (called middleware in Prisma)\\n- Connection pool\\n- Based on various community benchmarks, no dramatic performance differences\\n- All have huge amount of stars and downloads\\n\\nOverall, I found TypeORM and Sequelize to be a little more feature rich. For example, the following features are not supported only in Prisma: GIS queries, DB-level custom constraints, DB replication, soft delete, caching, exclude queries and some more\\n\\nWith that, shall we focus on what really set them apart and make a difference\\n\\n## What is fundamentally different?\\n\\n### 1. Type safety across the board\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** ORM\'s life is not easier since the TypeScript rise, to say the least. The need to support typed models/queries/etc yields a lot of developers sweat. Sequelize, for example, struggles to stabilize a TypeScript interface and, by now offers 3 different syntaxes + one external library ([sequelize-typescript](https://github.com/sequelize/sequelize-typescript)) that offers yet another style. Look at the syntax below, this feels like an afterthought - a library that was not built for TypeScript and now tries to squeeze it in somehow. Despite the major investment, both Sequelize and TypeORM offer only partial type safety. Simple queries do return typed objects, but other common corner cases like attributes/projections leave you with brittle strings. Here are a few examples:\\n\\n\\n```javascript\\n// Sequelize pesky TypeScript interface\\ntype OrderAttributes = {\\n id: number,\\n price: number,\\n // other attributes...\\n};\\n\\ntype OrderCreationAttributes = Optional;\\n\\n//\ud83d\ude2f Isn\'t this a weird syntax?\\nclass Order extends Model, InferCreationAttributes> {\\n declare id: CreationOptional;\\n declare price: number;\\n}\\n```\\n\\n```javascript\\n// Sequelize loose query types\\nawait getOrderModel().findAll({\\n where: { noneExistingField: \'noneExistingValue\' } //\ud83d\udc4d TypeScript will warn here\\n attributes: [\'none-existing-field\', \'another-imaginary-column\'], // No errors here although these columns do not exist\\n include: \'no-such-table\', //\ud83d\ude2f no errors here although this table doesn\'t exist\\n });\\n await getCountryModel().findByPk(\'price\'); //\ud83d\ude2f No errors here although the price column is not a primary key\\n```\\n\\n```javascript\\n// TypeORM loose query\\nconst ordersOnSales: Post[] = await orderRepository.find({\\n where: { onSale: true }, //\ud83d\udc4d TypeScript will warn here\\n select: [\'id\', \'price\'],\\n})\\nconsole.log(ordersOnSales[0].userId); //\ud83d\ude2f No errors here although the \'userId\' column is not part of the returned object\\n```\\n\\nIsn\'t it ironic that a library called **Type**ORM base its queries on strings?\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It takes a totally different approach by generating per-project client code that is fully typed. This client embodies types for everything: every query, relations, sub-queries, everything (except migrations). While other ORMs struggles to infer types from discrete models (including associations that are declared in other files), Prisma\'s offline code generation is easier: It can look through the entire DB relations, use custom generation code and build an almost perfect TypeScript experience. Why \'almost\' perfect? for some reason, Prisma advocates using plain SQL for migrations, which might result in a discrepancy between the code models and the DB schema. Other than that, this is how Prisma\'s client brings end to end type safety:\\n\\n```javascript\\nawait prisma.order.findMany({\\n where: {\\n noneExistingField: 1, //\ud83d\udc4d TypeScript error here\\n },\\n select: {\\n noneExistingRelation: { //\ud83d\udc4d TypeScript error here\\n select: { id: true }, \\n },\\n noneExistingField: true, //\ud83d\udc4d TypeScript error here\\n },\\n });\\n\\n await prisma.order.findUnique({\\n where: { price: 50 }, //\ud83d\udc4d TypeScript error here\\n });\\n```\\n\\n**\ud83d\udcca How important:** TypeScript support across the board is valuable for DX mostly. Luckily, we have another safety net: The project testing. Since tests are mandatory, having build-time type verification is important but not a life saver\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n## 2. Make you forget SQL\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Many avoid ORMs while preferring to interact with the DB using lower-level techniques. One of their arguments is against the efficiency of ORMs: Since the generated queries are not visible immediately to the developers, wasteful queries might get executed unknowingly. While all ORMs provide syntactic sugar over SQL, there are subtle differences in the level of abstraction. The more the ORM syntax resembles SQL, the more likely the developers will understand their own actions\\n\\nFor example, TypeORM\'s query builder looks like SQL broken into convenient functions\\n\\n```javascript\\nawait createQueryBuilder(\'order\')\\n .leftJoinAndSelect(\\n \'order.userId\',\\n \'order.productId\',\\n \'country.name\',\\n \'country.id\'\\n )\\n .getMany();\\n```\\n\\nA developer who read this code \ud83d\udc46 is likely to infer that a *join* query between two tables will get executed\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma\'s mission statement is to simplify DB work, the following statement is taken from their homepage:\\n\\n\\"We designed its API to be intuitive, both for SQL veterans and *developers brand new to databases*\\"\\n\\nBeing ambitious to appeal also to database layman, Prisma builds a syntax with a little bit higher abstraction, for example:\\n\\n```javascript\\nawait prisma.order.findMany({\\n select: {\\n userId: true,\\n productId: true,\\n country: {\\n select: { name: true, id: true },\\n },\\n },\\n});\\n\\n```\\n\\nNo join is reminded here also it fetches records from two related tables (order, and country). Could you guess what SQL is being produced here? how many queries? One right, a simple join? Surprise, actually, two queries are made. Prisma fires one query per-table here, as the join logic happens on the ORM client side (not inside the DB). But why?? in some cases, mostly where there is a lot of repetition in the DB cartesian join, querying each side of the relation is more efficient. But in other cases, it\'s not. Prisma arbitrarily chose what they believe will perform better in *most* cases. I checked, in my case it\'s *slower* than doing a one-join query on the DB side. As a developer, I would miss this deficiency due to the high-level syntax (no join is mentioned). My point is, Prisma sweet and simple syntax might be a bless for developer who are brand new to databases and aim to achieve a working solution in a short time. For the longer term, having full awareness of the DB interactions is helpful, other ORMs encourage this awareness a little better\\n\\n**\ud83d\udcca How important:** Any ORM will hide SQL details from their users - without developer\'s awareness no ORM will save the day\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Not necessarily\\n\\n## 3. Performance\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Speak to an ORM antagonist and you\'ll hear a common sensible argument: ORMs are much slower than a \'raw\' approach. To an extent, this is a legit observation as [most comparisons](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802) will show none-negligible differences between raw/query-builder and ORM.\\n\\n![raw is faster d](./pg-driver-is-faster.png)\\n*Example: a direct insert against the PG driver is much shorter [Source](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802)* \\n\\n It should also be noted that these benchmarks don\'t tell the entire story - on top of raw queries, every solution must build a mapper layer that maps the raw data to JS objects, nest the results, cast types, and more. This work is included within every ORM but not shown in benchmarks for the raw option. In reality, every team which doesn\'t use ORM would have to build their own small \\"ORM\\", including a mapper, which will also impact performance\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It was my hope to see a magic here, eating the ORM cake without counting the calories, seeing Prisma achieving an almost \'raw\' query speed. I had some good and logical reasons for this hope: Prisma uses a DB client built with Rust. Theoretically, it could serialize to and nest objects faster (in reality, this happens on the JS side). It was also built from the ground up and could build on the knowledge pilled in ORM space for years. Also, since it returns POJOs only (see bullet \'No Active Record here!\') - no time should be spent on decorating objects with ORM fields\\n\\nYou already got it, this hope was not fulfilled. Going with every community benchmark ([one](https://dev.to/josethz00/benchmark-prisma-vs-typeorm-3873), [two](https://github.com/edgedb/imdbench), [three](https://deepkit.io/library)), Prisma at best is not faster than the average ORM. What is the reason? I can\'t tell exactly but it might be due the complicated system that must support Go, future languages, MongoDB and other non-relational DBs\\n\\n![Prisma is not faster](./throughput-benchmark.png)\\n*Example: Prisma is not faster than others. It should be noted that in other benchmarks Prisma scores higher and shows an \'average\' performance [Source](https://github.com/edgedb/imdbench)*\\n\\n**\ud83d\udcca How important:** It\'s expected from ORM users to live peacefully with inferior performance, for many systems it won\'t make a great deal. With that, 10%-30% performance differences between various ORMs are not a key factor\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** No\\n\\n## 4. No active records here!\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Node in its early days was heavily inspired by Ruby (e.g., testing \\"describe\\"), many great patterns were embraced, [Active Record](https://en.wikipedia.org/wiki/Active_record_pattern) is not among the successful ones. What is this pattern about in a nutshell? say you deal with Orders in your system, with Active Record an Order object/class will hold both the entity properties, possible also some of the logic functions and also CRUD functions. Many find this pattern to be awful, why? ideally, when coding some logic/flow, one should not keep her mind busy with side effects and DB narratives. It also might be that accessing some property unconsciously invokes a heavy DB call (i.e., lazy loading). If not enough, in case of heavy logic, unit tests might be in order (i.e., read [\'selective unit tests\'](https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/)) - it\'s going to be much harder to write unit tests against code that interacts with the DB. In fact, all of the respectable and popular architecture (e.g., DDD, clean, 3-tiers, etc) advocate to \'isolate the domain\', separate the core/logic of the system from the surrounding technologies. With all of that said, both TypeORM and Sequelize support the Active Record pattern which is displayed in many examples within their documentation. Both also support other better patterns like the data mapper (see below), but they still open the door for doubtful patterns\\n\\n\\n```javascript\\n// TypeORM active records \ud83d\ude1f\\n\\n@Entity()\\nclass Order extends BaseEntity {\\n @PrimaryGeneratedColumn()\\n id: number\\n\\n @Column()\\n price: number\\n\\n @ManyToOne(() => Product, (product) => product.order)\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n // some logic here\\n orderToUpdate.status = \\"approval\\";\\n orderToUpdate.save(); \\n orderToUpdate.products.forEach((products) =>{ \\n\\n })\\n orderToUpdate.usedConnection = ? \\n }\\n}\\n\\n\\n\\n```\\n\\n**\ud83e\udd14 How Prisma is different:** The better alternative is the data mapper pattern. It acts as a bridge, an adapter, between simple object notations (domain objects with properties) to the DB language, typically SQL. Call it with a plain JS object, POJO, get it saved in the DB. Simple. It won\'t add functions to the result objects or do anything beyond returning pure data, no surprising side effects. In its purest sense, this is a DB-related utility and completely detached from the business logic. While both Sequelize and TypeORM support this, Prisma offers *only* this style - no room for mistakes.\\n\\n\\n```javascript\\n// Prisma approach with a data mapper \ud83d\udc4d\\n\\n// This was generated automatically by Prisma\\ntype Order {\\n id: number\\n\\n price: number\\n\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n orderToUpdate.status = \\"approval\\";\\n prisma.order.update({ where: { id: orderToUpdate.id }, data: orderToUpdate }); \\n // Side effect \ud83d\udc46, but an explicit one. The thoughtful coder will move this to another function. Since it\'s happening outside, mocking is possible \ud83d\udc4d\\n products.forEach((products) =>{ // No lazy loading, the data is already here \ud83d\udc4d\\n\\n })\\n } \\n}\\n```\\n\\n In [Practica.js](https://github.com/practicajs/practica) we take it one step further and put the prisma models within the \\"DAL\\" layer and wrap it with the [repository pattern](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design). You may glimpse [into the code here](https://github.com/practicajs/practica/blob/21ff12ba19cceed9a3735c09d48184b5beb5c410/src/code-templates/services/order-service/domain/new-order-use-case.ts#L21), this is the business flow that calls the DAL layer\\n\\n\\n**\ud83d\udcca How important:** On the one hand, this is a key architectural principle to follow but the other hand most ORMs *allow* doing it right\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Yes!\\n\\n## 5. Documentation and developer-experience\\n\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** TypeORM and Sequelize documentation is mediocre, though TypeORM is a little better. Based on my personal experience they do get a little better over the years, but still by no mean they deserve to be called \\"good\\" or \\"great\\". For example, if you seek to learn about \'raw queries\' - Sequelize offers [a very short page](https://sequelize.org/docs/v6/core-concepts/raw-queries/) on this matter, TypeORM info is spread in multiple other pages. Looking to learn about pagination? Couldn\'t find Sequelize documents, TypeORM has [some short explanation](https://typeorm.io/select-query-builder#using-pagination), 150 words only\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma documentation rocks! See their documents on similar topics: [raw queries](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access) and [pagingation](https://www.prisma.io/docs/concepts/components/prisma-client/pagination), thousands of words, and dozens of code examples. The writing itself is also great, feels like some professional writers were involved\\n\\n![Prisma docs are comprehensive](./count-docs.png)\\n \\nThis chart above shows how comprehensive are Prisma docs (Obviously this by itself doesn\'t prove quality)\\n\\n**\ud83d\udcca How important:** Great docs are a key to awareness and avoiding pitfalls\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** You bet\\n\\n## 6. Observability, metrics, and tracing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Good chances are (say about 99.9%) that you\'ll find yourself diagnostic slow queries in production or any other DB-related quirks. What can you expect from traditional ORMs in terms of observability? Mostly logging. [Sequelize provides both logging](https://sequelize.org/api/v7/interfaces/queryoptions#benchmark) of query duration and programmatic access to the connection pool state ({size,available,using,waiting}). [TypeORM provides only logging](https://orkhan.gitbook.io/typeorm/docs/logging) of queries that suppress a pre-defined duration threshold. This is better than nothing, but assuming you don\'t read production logs 24/7, you\'d probably need more than logging - an alert to fire when things seem faulty. To achieve this, it\'s your responsibility to bridge this info to your preferred monitoring system. Another logging downside for this sake is verbosity - we need to emit tons of information to the logs when all we really care for is the average duration. Metrics can serve this purpose much better as we\'re about to see soon with Prisma\\n\\nWhat if you need to dig into which specific part of the query is slow? unfortunately, there is no breakdown of the query phases duration - it\'s being left to you as a black-box\\n\\n```javascript\\n// Sequelize - logging various DB information\\n\\n```\\n\\n![Logging query duration](./sequelize-log.png)\\nLogging each query in order to realize trends and anomaly in the monitoring system\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Since Prisma targets also enterprises, it must bring strong ops capabilities. Beautifully, it packs support for both [metrics](https://www.prisma.io/docs/concepts/components/prisma-client/metrics) and [open telemetry tracing](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing)!. For metrics, it generates custom JSON with metric keys and values so anyone can adapt this to any monitoring system (e.g., CloudWatch, statsD, etc). On top of this, it produces out of the box metrics in [Prometheus](https://prometheus.io/) format (one of the most popular monitoring platforms). For example, the metric \'prisma_client_queries_duration_histogram_ms\' provides the average query length in the system overtime. What is even more impressive is the support for open-tracing - it feeds your OpenTelemetry collector with spans that describe the various phases of every query. For example, it might help realize what is the bottleneck in the query pipeline: Is it the DB connection, the query itself or the serialization?\\n\\n![prisma tracing](./trace-diagram.png)\\nPrisma visualizes the various query phases duration with open-telemtry \\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n\\n**\ud83d\udcca How important:** Goes without words how impactful is observability, however filling the gap in other ORM will demand no more than a few days\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n## 7. Continuity - will it be here with us in 2024/2025\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** We live quite peacefully with the risk of one of our dependencies to disappear. With ORM though, this risk demand special attention because our buy-in is higher (i.e., harder to replace) and maintaining it was proven to be harder. Just look at a handful of successful ORMs in the past: objection.js, waterline, bookshelf - all of these respectful project had 0 commits in the past month. The single maintainer of objection.js [announced that he won\'t work the project anymore](https://github.com/Vincit/objection.js/issues/2335). This high churn rate is not surprising given the huge amount of moving parts to maintain, the gazillion corner cases and the modest \'budget\' OSS projects live with. Looking at OpenCollective shows that [Sequelize](https://opencollective.com/sequelize#category-BUDGET) and [TypeORM](https://opencollective.com/typeorm) are funded with ~1500$ month in average. This is barely enough to cover a daily Starbucks cappuccino and croissant (6.95$ x 365) for 5 maintainers. Nothing contrasts this model more than a startup company that just raised its series B - Prisma is [funded with 40,000,000$ (40 millions)](https://www.prisma.io/blog/series-b-announcement-v8t12ksi6x#:~:text=We%20are%20excited%20to%20announce,teams%20%26%20organizations%20in%20this%20article.) and recruited 80 people! Should not this inspire us with high confidence about their continuity? I\'ll surprisingly suggest that quite the opposite is true\\n\\nSee, an OSS ORM has to go over one huge hump, but a startup company must pass through TWO. The OSS project will struggle to achieve the critical mass of features, including some high technical barriers (e.g., TypeScript support, ESM). This typically lasts years, but once it does - a project can focus mostly on maintenance and step out of the danger zone. The good news for TypeORM and Sequelize is that they already did! Both struggled to keep their heads above the water, there were rumors in the past that [TypeORM is not maintained anymore](https://github.com/typeorm/typeorm/issues/3267), but they managed to go through this hump. I counted, both projects had approximately ~2000 PRs in the past 3 years! Going with [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize), each see multiple commits every week. They both have vibrant traction, and the majority of features you would expect from an ORM. TypeORM even supports beyond-the-basics features like multi data source and caching. It\'s unlikely that now, once they reached the promise land - they will fade away. It might happen, there is no guarantee in the OSS galaxy, but the risk is low\\n\\n![One hump](./one-hump.png)\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma a little lags behind in terms of features, but with a budget of 40M$ - there are good reasons to believe that they will pass the first hump, achieving a critical mass of features. I\'m more concerned with the second hump - showing revenues in 2 years or saying goodbye. As a company that is backed by venture capitals - the model is clear and cruel: In order to secure their next round, series B or C (depends whether the seed is counted), there must be a viable and proven business model. How do you \'sell\' ORM? Prisma experiments with multiple products, none is mature yet or being paid for. How big is this risk? According to [this startup companies success statistics](https://spdload.com/blog/startup-success-rate/), \\"About 65% of the Series A startups get series B, while 35% of the companies that get series A fail.\\". Since Prisma already gained a lot of love and adoption from the community, there success chances are higher than the average round A/B company, but even 20% or 10% chances to fade away is concerning\\n\\n> This is terrifying news - companies happily choose a young commercial OSS product without realizing that there are 10-30% chances for this product to disappear\\n\\n\\n![Two humps](./two-humps.png)\\n\\nSome of startup companies who seek a viable business model do not shut the doors rather change the product, the license or the free features. This is not my subjective business analysis, here are few examples: [MongoDB changed their license](https://techcrunch.com/2018/10/16/mongodb-switches-up-its-open-source-license/), this is why the majority had to host their Mongo DB over a single vendor. [Redis did something similar](https://techcrunch.com/2019/02/21/redis-labs-changes-its-open-source-license-again/). What are the chances of Prisma pivoting to another type of product? It actually already happened before, Prisma 1 was mostly about graphQL client and server, [it\'s now retired](https://github.com/prisma/prisma1)\\n\\nIt\'s just fair to mention the other potential path - most round B companies do succeed to qualify for the next round, when this happens even bigger money will be involved in building the \'Ferrari\' of JavaScript ORMs. I\'m surely crossing my fingers for these great people, at the same time we have to be conscious about our choices\\n\\n**\ud83d\udcca How important:** As important as having to code again the entire DB layer in a big system\\n\\n![Medium importance](./high2-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Quite the opposite\\n\\n## Closing - what should you use now?\\n\\nBefore proposing my key take away - which is the primary ORM, let\'s repeat the key learning that were introduced here:\\n\\n1. \ud83e\udd47 Prisma deserves a medal for its awesome DX, documentation, observability support and end-to-end TypeScript coverage\\n2. \ud83e\udd14 There are reasons to be concerned about Prisma\'s business continuity as a young startup without a viable business model. Also Prisma\'s abstract client syntax might blind developers a little more than other ORMs\\n3. \ud83c\udfa9 The contenders, TypeORM and Sequelize, matured and doing quite well: both have merged thousand PRs in the past 3 years to become more stable, they keep introducing new releases (see [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize)), and for now holds more features than Prisma. Also, both show solid performance (for an ORM). Hats off to the maintainers!\\n\\nBased on these observations, which should you pick? which ORM will we use for [practica.js](https://github.com/practicajs/practica)?\\n \\nPrisma is an excellent addition to Node.js ORMs family, but not the hassle-free one tool to rule them all. It\'s a mixed bag of many delicious candies and a few gotchas. Wouldn\'t it grow to tick all the boxes? Maybe, but unlikely. Once built, it\'s too hard to dramatically change the syntax and engine performance. Then, during the writing and speaking with the community, including some Prisma enthusiasts, I realized that it doesn\'t aim to be the can-do-everything \'Ferrari\'. Its positioning seems to resemble more a convenient family car with a solid engine and awesome user experience. In other words, it probably aims for the enterprise space where there is mostly demand for great DX, OK performance, and business-class support\\n\\nIn the end of this journey I see no dominant flawless \'Ferrari\' ORM. I should probably change my perspective: Building ORM for the hectic modern JavaScript ecosystem is 10x harder than building a Java ORM back then in 2001. There is no stain in the shirt, it\'s a cool JavaScript swag. I learned to accept what we have, a rich set of features, tolerable performance, good enough for many systems. Need more? Don\'t use ORM. Nothing is going to change dramatically, it\'s now as good as it can be\\n\\n### When will it shine?\\n\\n**Surely use Prisma under these scenarios -** If your data needs are rather simple; when time-to-market concern takes precedence over the data processing accuracy; when the DB is relatively small; if you\'re a mobile/frontend developer who is doing her first steps in the backend world; when there is a need for business-class support; AND when Prisma\'s long term business continuity risk is a non-issue for you\\n\\n**I\'d probably prefer other options under these conditions -** If the DB layer performance is a major concern; if you\'re savvy backend developer with solid SQL capabilities; when there is a need for fine grain control over the data layer. For all of these cases, Prisma might still work, but my primary choices would be using knex/TypeORM/Sequelize with a data-mapper style\\n\\nConsequently, we love Prisma and add it behind flag (--orm=prisma) to Practica.js. At the same time, until some clouds will disappear, Sequelize will remain our default ORM\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Popular Node.js patterns and tools to re-consider](https://practica.dev/blog/popular-nodejs-pattern-and-tools-to-reconsider)\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"popular-nodejs-pattern-and-tools-to-reconsider","metadata":{"permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/pattern-to-reconsider/index.md","source":"@site/blog/pattern-to-reconsider/index.md","title":"Popular Node.js patterns and tools to re-consider","description":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?","date":"2022-08-02T10:00:00.000Z","formattedDate":"August 2, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":21.09,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"popular-nodejs-pattern-and-tools-to-reconsider","date":"2022-08-02T10:00","hide_table_of_contents":true,"title":"Popular Node.js patterns and tools to re-consider","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"},"nextItem":{"title":"Practica.js v0.0.1 is alive","permalink":"/blog/practica-is-alive"}},"content":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?\\n\\nIn his novel book \'Atomic Habits\' the author James Clear states that:\\n\\n> \\"Mastery is created by habits. However, sometimes when we\'re on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot\\". In other words, practice makes perfect, and bad practices make things worst\\n\\nWe copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change\\n\\nLuckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples. \\n\\nAre those disruptive thoughts surely correct? I\'m not sure. There is one things I\'m sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not \\"don\'t use this tool!\\" but rather becoming familiar with other techniques that, _under some circumstances_ might be a better fit\\n\\n![Animals and frameworks shed their skin](./crab.webp)\\n\\n_The True Crab\'s exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell_\\n\\n\\n## TOC - Patterns to reconsider\\n\\n1. Dotenv\\n2. Calling a service from a controller\\n3. Nest.js dependency injection for all classes\\n4. Passport.js\\n5. Supertest\\n6. Fastify utility decoration\\n7. Logging from a catch clause\\n8. Morgan logger\\n9. NODE_ENV\\n\\n\x3c!--truncate--\x3e\\n## 1. Dotenv as your configuration source\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** A super popular technique in which the app configurable values (e.g., DB user name) are stored in a simple text file. Then, when the app loads, the dotenv library sets all the text file values as environment variables so the code can read this\\n\\n```javascript\\n// .env file\\nUSER_SERVICE_URL=https://users.myorg.com\\n\\n//start.js\\nrequire(\'dotenv\').config();\\n\\n//blog-post-service.js\\nrepository.savePost(post);\\n//update the user number of posts, read the users service URL from an environment variable\\nawait axios.put(`${process.env.USER_SERVICE_URL}/api/user/${post.userId}/incrementPosts`)\\n\\n```\\n\\n**\ud83d\udcca How popular:** 21,806,137 downloads/week!\\n\\n**\ud83e\udd14 Why it might be wrong:** Dotenv is so easy and intuitive to start with, so one might easily overlook fundamental features: For example, it\'s hard to infer the configuration schema and realize the meaning of each key and its typing. Consequently, there is no built-in way to fail fast when a mandatory key is missing - a flow might fail after starting and presenting some side effects (e.g., DB records were already mutated before the failure). In the example above, the blog post will be saved to DB, and only then will the code realize that a mandatory key is missing - This leaves the app hanging in an invalid state. On top of this, in the presence of many keys, it\'s impossible to organize them hierarchically. If not enough, it encourages developers to commit this .env file which might contain production values - this happens because there is no clear way to define development defaults. Teams usually work around this by committing .env.example file and then asking whoever pulls code to rename this file manually. If they remember to of course\\n\\n**\u2600\ufe0f Better alternative:** Some configuration libraries provide out of the box solution to all of these needs. They encourage a clear schema and the possibility to validate early and fail if needed. See [comparison of options here](https://practica.dev/decisions/configuration-library). One of the better alternatives is [\'convict\'](https://github.com/mozilla/node-convict), down below is the same example, this time with Convict, hopefully it\'s better now:\\n\\n```javascript\\n// config.js\\nexport default {\\n userService: {\\n url: {\\n // Hierarchical, documented and strongly typed \ud83d\udc47\\n doc: \\"The URL of the user management service including a trailing slash\\",\\n format: \\"url\\",\\n default: \\"http://localhost:4001\\",\\n nullable: false,\\n env: \\"USER_SERVICE_URL\\",\\n },\\n },\\n //more keys here\\n};\\n\\n//start.js\\nimport convict from \\"convict\\";\\nimport configSchema from \\"config\\";\\nconvict(configSchema);\\n// Fail fast!\\nconvictConfigurationProvider.validate();\\n\\n//blog-post.js\\nrepository.savePost(post);\\n// Will never arrive here if the URL is not set\\nawait axios.put(\\n `${convict.get(userService.url)}/api/user/${post.userId}/incrementPosts`\\n);\\n```\\n\\n## 2. Calling a \'fat\' service from the API controller\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Consider a reader of our code who wishes to understand the entire _high-level_ flow or delve into a very _specific_ part. She first lands on the API controller, where requests start. Unlike what its name implies, this controller layer is just an adapter and kept really thin and straightforward. Great thus far. Then the controller calls a big \'service\' with thousands of lines of code that represent the entire logic\\n\\n```javascript\\n// user-controller\\nrouter.post(\'/\', async (req, res, next) => {\\n await userService.add(req.body);\\n // Might have here try-catch or error response logic\\n}\\n\\n// user-service\\nexports function add(newUser){\\n // Want to understand quickly? Need to understand the entire user service, 1500 loc\\n // It uses technical language and reuse narratives of other flows\\n this.copyMoreFieldsToUser(newUser)\\n const doesExist = this.updateIfAlreadyExists(newUser)\\n if(!doesExist){\\n addToCache(newUser);\\n }\\n // 20 more lines that demand navigating to other functions in order to get the intent\\n}\\n\\n\\n```\\n\\n**\ud83d\udcca How popular:** It\'s hard to pull solid numbers here, I could confidently say that in _most_ of the app that I see, this is the case\\n\\n**\ud83e\udd14 Why it might be wrong:** We\'re here to tame complexities. One of the useful techniques is deferring a complexity to the later stage possible. In this case though, the reader of the code (hopefully) starts her journey through the tests and the controller - things are simple in these areas. Then, as she lands on the big service - she gets tons of complexity and small details, although she is focused on understanding the overall flow or some specific logic. This is **unnecessary** complexity\\n\\n**\u2600\ufe0f Better alternative:** The controller should call a particular type of service, a **use-case** , which is responsible for _summarizing_ the flow in a business and simple language. Each flow/feature is described using a use-case, each contains 4-10 lines of code, that tell the story without technical details. It mostly orchestrates other small services, clients, and repositories that hold all the implementation details. With use cases, the reader can grasp the high-level flow easily. She can now **choose** where she would like to focus. She is now exposed only to **necessary** complexity. This technique also encourages partitioning the code to the smaller object that the use-case orchestrates. Bonus: By looking at coverage reports, one can tell which features are covered, not just files/functions\\n\\nThis idea by the way is formalized in the [\'clean architecture\' book](https://www.bookdepository.com/Clean-Architecture-Robert-Martin/9780134494166?redirected=true&utm_medium=Google&utm_campaign=Base1&utm_source=IL&utm_content=Clean-Architecture&selectCurrency=ILS&w=AFF9AU99ZB4MTDA8VTRQ&gclid=Cj0KCQjw3eeXBhD7ARIsAHjssr92kqLn60dnfQCLjbkaqttdgvhRV5dqKtnY680GCNDvKp-16HtZp24aAg6GEALw_wcB) - I\'m not a big fan of \'fancy\' architectures, but see - it\'s worth cherry-picking techniques from every source. You may walk-through our [Node.js best practices starter, practica.js](https://github.com/practicajs/practica), and examine the use-cases code\\n\\n```javascript\\n// add-order-use-case.js\\nexport async function addOrder(newOrder: addOrderDTO) {\\n orderValidation.assertOrderIsValid(newOrder);\\n const userWhoOrdered = await userServiceClient.getUserWhoOrdered(\\n newOrder.userId\\n );\\n paymentTermsService.assertPaymentTerms(\\n newOrder.paymentTermsInDays,\\n userWhoOrdered.terms\\n );\\n\\n const response = await orderRepository.addOrder(newOrder);\\n\\n return response;\\n}\\n```\\n\\n## 3. Nest.js: Wire _everything_ with dependency injection\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** If you\'re doing Nest.js, besides having a powerful framework in your hands, you probably use DI for _everything_ and make every class injectable. Say you have a weather-service that depends upon humidity-service, and **there is no requirement to swap** the humidity-service with alternative providers. Nevertheless, you inject humidity-service into the weather-service. It becomes part of your development style, \\"why not\\" you think - I may need to stub it during testing or replace it in the future\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\n@Injectable()\\nexport class GoogleHumidityService {\\n\\n async getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n }\\n}\\n\\n// weather-service.ts - customer facing\\nimport { GoogleHumidityService } from \'./humidity-service.ts\';\\n\\nexport type weatherInfo{\\n temperature: number,\\n humidity: number\\n}\\n\\nexport class WeatherService {\\n constructor(private humidityService: GoogleHumidityService) {}\\n\\n async GetWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n }\\n}\\n\\n// app.module.ts\\n@Module({\\n providers: [GoogleHumidityService, WeatherService],\\n})\\nexport class AppModule {}\\n```\\n\\n**\ud83d\udcca How popular:** No numbers here but I could confidently say that in _all_ of the Nest.js app that I\'ve seen, this is the case. In the popular [\'nestjs-realworld-example-ap[p\'](](https://github.com/lujakob/nestjs-realworld-example-app)) all the services are \'injectable\'\\n\\n**\ud83e\udd14 Why it might be wrong:** Dependency injection is not a priceless coding style but a pattern you should pull in the right moment, like any other pattern. Why? Because any pattern has a price. What price, you ask? First, encapsulation is violated. Clients of the weather-service are now aware that other providers are being used _internally_. Some clients may get tempted to override providers also it\'s not under their responsibility. Second, it\'s another layer of complexity to learn, maintain, and one more way to shoot yourself in the legs. StackOverflow owes some of its revenues to Nest.js DI - plenty of discussions try to solve this puzzle (e.g. did you know that in case of circular dependencies the order of imports matters?). Third, there is the performance thing - Nest.js, for example struggled to provide a decent start time for serverless environments and had to introduce [lazy loaded modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules). Don\'t get me wrong, **in some cases**, there is a good case for DI: When a need arises to decouple a dependency from its caller, or to allow clients to inject custom implementations (e.g., the strategy pattern). **In such case**, when there is a value, you may consider whether the _value of DI is worth its price_. If you don\'t have this case, why pay for nothing?\\n\\nI recommend reading the first paragraphs of this blog post [\'Dependency Injection is EVIL\'](https://www.tonymarston.net/php-mysql/dependency-injection-is-evil.html) (and absolutely don\'t agree with this bold words)\\n\\n**\u2600\ufe0f Better alternative:** \'Lean-ify\' your engineering approach - avoid using any tool unless it serves a real-world need immediately. Start simple, a dependent class should simply import its dependency and use it - Yeah, using the plain Node.js module system (\'require\'). Facing a situation when there is a need to factor dynamic objects? There are a handful of simple patterns, simpler than DI, that you should consider, like \'if/else\', factory function, and more. Are singletons requested? Consider techniques with lower costs like the module system with factory function. Need to stub/mock for testing? Monkey patching might be better than DI: better clutter your test code a bit than clutter your production code. Have a strong need to hide from an object where its dependencies are coming from? You sure? Use DI!\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\nexport async function getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n}\\n\\n// weather-service.ts - customer facing\\nimport { getHumidity } from \\"./humidity-service.ts\\";\\n\\n// \u2705 No wiring is happening externally, all is flat and explicit. Simple\\nexport async function getWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n // Nobody needs to know about it, its an implementation details\\n await getHumidity(when);\\n}\\n```\\n\\n___\\n\\n## 1 min pause: A word or two about me, the author\\n\\nMy name is Yoni Goldberg, I\'m a Node.js developer and consultant. I wrote few code-books like [JavaScript testing best practices](https://github.com/goldbergyoni/javascript-testing-best-practices) and [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices) (100,000 stars \u2728\ud83e\udd79). That said, my best guide is [Node.js testing practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices) which only few read \ud83d\ude1e. I shall release [an advanced Node.js testing course soon](https://testjavascript.com/) and also hold workshops for teams. I\'m also a core maintainer of [Practica.js](https://github.com/practicajs/practica) which is a Node.js starter that creates a production-ready example Node Monorepo solution that is based on the standards and simplicity. It might be your primary option when starting a new Node.js solution\\n\\n___\\n\\n## 4. Passport.js for token authentication\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Commonly, you\'re in need to issue or/and authenticate JWT tokens. Similarly, you might need to allow login from _one_ single social network like Google/Facebook. When faced with these kinds of needs, Node.js developers rush to the glorious library [Passport.js](https://www.passportjs.org/) like butterflies are attracted to light\\n\\n**\ud83d\udcca How popular:** 1,389,720 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** When tasked with guarding your routes with JWT token - you\'re just a few lines of code shy from ticking the goal. Instead of messing up with a new framework, instead of introducing levels of indirections (you call passport, then it calls you), instead of spending time learning new abstractions - use a JWT library directly. Libraries like [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) or [fast-jwt](https://github.com/nearform/fast-jwt) are simple and well maintained. Have concerns with the security hardening? Good point, your concerns are valid. But would you not get better hardening with a direct understanding of your configuration and flow? Will hiding things behind a framework help? Even if you prefer the hardening of a battle-tested framework, Passport doesn\'t handle a handful of security risks like secrets/token, secured user management, DB protection, and more. My point, you probably anyway need fully-featured user and authentication management platforms. Various cloud services and OSS projects, can tick all of those security concerns. Why then start in the first place with a framework that doesn\'t satisfy your security needs? It seems like many who opt for Passport.js are not fully aware of which needs are satisfied and which are left open. All of that said, Passport definitely shines when looking for a quick way to support _many_ social login providers\\n\\n**\u2600\ufe0f Better alternative:** Is token authentication in order? These few lines of code below might be all you need. You may also glimpse into [Practica.js wrapper around these libraries](https://github.com/practicajs/practica/tree/main/src/code-templates/libraries/jwt-token-verifier). A real-world project at scale typically need more: supporting async JWT [(JWKS)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets), securely manage and rotate the secrets to name a few examples. In this case, OSS solution like [keycloak (https://github.com/keycloak/keycloak) or commercial options like Auth0[https://github.com/auth0] are alternatives to consider\\n\\n```javascript\\n// jwt-middleware.js, a simplified version - Refer to Practica.js to see some more corner cases\\nconst middleware = (req, res, next) => {\\n if(!req.headers.authorization){\\n res.sendStatus(401)\\n }\\n\\n jwt.verify(req.headers.authorization, options.secret, (err: any, jwtContent: any) => {\\n if (err) {\\n return res.sendStatus(401);\\n }\\n\\n req.user = jwtContent.data;\\n\\n next();\\n });\\n```\\n\\n## 5. Supertest for integration/API testing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** When testing against an API (i.e., component, integration, E2E tests), the library [supertest](https://www.npmjs.com/package/supertest) provides a sweet syntax that can both detect the web server address, make HTTP call and also assert on the response. Three in one\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400\\", (done) => {\\n const request = require(\\"supertest\\");\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n request(app)\\n .post(\\"/user\\")\\n .send(userToAdd)\\n .expect(\\"Content-Type\\", /json/)\\n .expect(400, done);\\n\\n // Assert\\n // We already asserted above \u261d\ud83c\udffb as part of the request\\n});\\n```\\n\\n**\ud83d\udcca How popular:** 2,717,744 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** You already have your assertion library (Jest? Chai?), it has a great error highlighting and comparison - you trust it. Why code some tests using another assertion syntax? Not to mention, Supertest\'s assertion errors are not as descriptive as Jest and Chai. It\'s also cumbersome to mix HTTP client + assertion library instead of choosing the best for each mission. Speaking of the best, there are more standard, popular, and better-maintained HTTP clients (like fetch, axios and other friends). Need another reason? Supertest might encourage coupling the tests to Express as it offers a constructor that gets an Express object. This constructor infers the API address automatically (useful when using dynamic test ports). This couples the test to the implementation and won\'t work in the case where you wish to run the same tests against a remote process (the API doesn\'t live with the tests). My repository [\'Node.js testing best practices\'](https://github.com/testjavascript/nodejs-integration-tests-best-practices) holds examples of how tests can infer the API port and address\\n\\n**\u2600\ufe0f Better alternative:** A popular and standard HTTP client library like Node.js Fetch or Axios. In [Practica.js](https://github.com/practicajs/practica) (a Node.js starter that packs many best practices) we use Axios. It allows us to configure a HTTP client that is shared among all the tests: We bake inside a JWT token, headers, and a base URL. Another good pattern that we look at, is making each Microservice generate HTTP client library for its consumers. This brings strong-type experience to the clients, synchronizes the provider-consumer versions and as a bonus - The provider can test itself with the same library that its consumers are using\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400 and includes a reason\\", (done) => {\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n const receivedResponse = axios.post(\\n `http://localhost:${apiPort}/user`,\\n userToAdd\\n );\\n\\n // Assert\\n // \u2705 Assertion happens in a dedicated stage and a dedicated library\\n expect(receivedResponse).toMatchObject({\\n status: 400,\\n data: {\\n reason: \\"no-name\\",\\n },\\n });\\n});\\n```\\n\\n## 6. Fastify decorate for non request/web utilities\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** [Fastify](https://github.com/fastify/fastify) introduces great patterns. Personally, I highly appreciate how it preserves the simplicity of Express while bringing more batteries. One thing that got me wondering is the \'decorate\' feature which allows placing common utilities/services inside a widely accessible container object. I\'m referring here specifically to the case where a cross-cutting concern utility/service is being used. Here is an example:\\n\\n```javascript\\n// An example of a utility that is cross-cutting-concern. Could be logger or anything else\\nfastify.decorate(\'metricsService\', function (name) {\\n fireMetric: () => {\\n // My code that sends metrics to the monitoring system\\n }\\n})\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n this.metricsService.fireMetric({name: \'new-request\'})\\n // Handle the request\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n // How to fire a metric?\\n}\\n```\\n\\nIt should be noted that \'decoration\' is also used to place values (e.g., user) inside a request - this is a slightly different case and a sensible one\\n\\n**\ud83d\udcca How popular:** Fastify has 696,122 weekly download and growing rapidly. The decorator concept is part of the framework\'s core\\n\\n**\ud83e\udd14 Why it might be wrong:** Some services and utilities serve cross-cutting-concern needs and should be accessible from other layers like domain (i.e, business logic, DAL). When placing utilities inside this object, the Fastify object might not be accessible to these layers. You probably don\'t want to couple your web framework with your business logic: Consider that some of your business logic and repositories might get invoked from non-REST clients like CRON, MQ, and similar - In these cases, Fastify won\'t get involved at all so better not trust it to be your service locator\\n\\n**\u2600\ufe0f Better alternative:** A good old Node.js module is a standard way to expose and consume functionality. Need a singleton? Use the module system caching. Need to instantiate a service in correlation with a Fastify life-cycle hook (e.g., DB connection on start)? Call it from that Fastify hook. In the rare case where a highly dynamic and complex instantiation of dependencies is needed - DI is also a (complex) option to consider\\n\\n```javascript\\n// \u2705 A simple usage of good old Node.js modules\\n// metrics-service.js\\n\\nexports async function fireMetric(name){\\n // My code that sends metrics to the monitoring system\\n}\\n\\nimport {fireMetric} from \'./metrics-service.js\'\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n metricsService.fireMetric({name: \'new-request\'})\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n metricsService.fireMetric({name: \'new-request\'})\\n}\\n```\\n\\n## 7. Logging from a catch clause\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** You catch an error somewhere deep in the code (not on the route level), then call logger.error to make this error observable. Seems simple and necessary\\n\\n```javascript\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n logger.error(error, this, {operation: addNewOrder});\\n}\\n```\\n\\n**\ud83d\udcca How popular:** Hard to put my hands on numbers but it\'s quite popular, right?\\n\\n**\ud83e\udd14 Why it might be wrong:** First, errors should get handled/logged in a central location. Error handling is a critical path. Various catch clauses are likely to behave differently without a centralized and unified behavior. For example, a request might arise to tag all errors with certain metadata, or on top of logging, to also fire a monitoring metric. Applying these requirements in ~100 locations is not a walk in the park. Second, catch clauses should be minimized to particular scenarios. By default, the natural flow of an error is bubbling down to the route/entry-point - from there, it will get forwarded to the error handler. Catch clauses are more verbose and error-prone - therefore it should serve two very specific needs: When one wishes to change the flow based on the error or enrich the error with more information (which is not the case in this example)\\n\\n**\u2600\ufe0f Better alternative:** By default, let the error bubble down the layers and get caught by the entry-point global catch (e.g., Express error middleware). In cases when the error should trigger a different flow (e.g., retry) or there is value in enriching the error with more context - use a catch clause. In this case, ensure the .catch code also reports to the error handler\\n\\n```javascript\\n// A case where we wish to retry upon failure\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n // \u2705 A central location that handles error\\n errorHandler.handle(error, this, {operation: addNewOrder});\\n callTheUserService(numOfRetries++);\\n}\\n```\\n\\n## 8. Use Morgan logger for express web requests\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** In many web apps, you are likely to find a pattern that is being copy-pasted for ages - Using Morgan logger to log requests information:\\n\\n```javascript\\nconst express = require(\\"express\\");\\nconst morgan = require(\\"morgan\\");\\n\\nconst app = express();\\n\\napp.use(morgan(\\"combined\\"));\\n```\\n\\n**\ud83d\udcca How popular:** 2,901,574 downloads/week\\n\\n**\ud83e\udd14 Why it might be wrong:** Wait a second, you already have your main logger, right? Is it Pino? Winston? Something else? Great. Why deal with and configure yet another logger? I do appreciate the HTTP domain-specific language (DSL) of Morgan. The syntax is sweet! But does it justify having two loggers?\\n\\n**\u2600\ufe0f Better alternative:** Put your chosen logger in a middleware and log the desired request/response properties:\\n\\n```javascript\\n// \u2705 Use your preferred logger for all the tasks\\nconst logger = require(\\"pino\\")();\\napp.use((req, res, next) => {\\n res.on(\\"finish\\", () => {\\n logger.info(`${req.url} ${res.statusCode}`); // Add other properties here\\n });\\n next();\\n});\\n```\\n\\n## 9. Having conditional code based on `NODE_ENV` value\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** To differentiate between development vs production configuration, it\'s common to set the environment variable NODE_ENV with \\"production|test\\". Doing so allows the various tooling to act differently. For example, some templating engines will cache compiled templates only in production. Beyond tooling, custom applications use this to specify behaviours that are unique to the development or production environment:\\n\\n```javascript\\nif (process.env.NODE_ENV === \\"production\\") {\\n // This is unlikely to be tested since test runner usually set NODE_ENV=test\\n setLogger({ stdout: true, prettyPrint: false });\\n // If this code branch above exists, why not add more production-only configurations:\\n collectMetrics();\\n} else {\\n setLogger({ splunk: true, prettyPrint: true });\\n}\\n```\\n\\n**\ud83d\udcca How popular:** 5,034,323 code results in GitHub when searching for \\"NODE_ENV\\". It doesn\'t seem like a rare pattern\\n\\n**\ud83e\udd14 Why it might be wrong:** Anytime your code checks whether it\'s production or not, this branch won\'t get hit by default in some test runner (e.g., Jest set `NODE_ENV=test`). In _any_ test runner, the developer must remember to test for each possible value of this environment variable. In the example above, `collectMetrics()` will be tested for the first time in production. Sad smiley. Additionally, putting these conditions opens the door to add more differences between production and the developer machine - when this variable and conditions exists, a developer gets tempted to put some logic for production only. Theoretically, this can be tested: one can set `NODE_ENV = \\"production\\"` in testing and cover the production branches (if she remembers...). But then, if you can test with `NODE_ENV=\'production\'`, what\'s the point in separating? Just consider everything to be \'production\' and avoid this error-prone mental load\\n\\n**\u2600\ufe0f Better alternative:** Any code that was written by us, must be tested. This implies avoiding any form of if(production)/else(development) conditions. Wouldn\'t anyway developers machine have different surrounding infrastructure than production (e.g., logging system)? They do, the environments are quite difference, but we feel comfortable with it. These infrastructural things are battle-tested, extraneous, and not part of our code. To keep the same code between dev/prod and still use different infrastructure - we put different values in the configuration (not in the code). For example, a typical logger emits JSON in production but in a development machine it emits \'pretty-print\' colorful lines. To meet this, we set ENV VAR that tells whether what logging style we aim for:\\n\\n```javascript\\n//package.json\\n\\"scripts\\": {\\n \\"start\\": \\"LOG_PRETTY_PRINT=false index.js\\",\\n \\"test\\": \\"LOG_PRETTY_PRINT=true jest\\"\\n}\\n\\n//index.js\\n//\u2705 No condition, same code for all the environments. The variations are defined externally in config or deployment files\\nsetLogger({prettyPrint: process.env.LOG_PRETTY_PRINT})\\n```\\n\\n## Closing\\n\\nI hope that these thoughts, at least one of them, made you re-consider adding a new technique to your toolbox. In any case, let\'s keep our community vibrant, disruptive and kind. Respectful discussions are almost as important as the event loop. Almost.\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [How to be a better Node.js developer in 2020](https://yonigoldberg.medium.com/20-ways-to-become-a-better-node-js-developer-in-2020-d6bd73fcf424). The 2023 version is coming soon\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"practica-is-alive","metadata":{"permalink":"/blog/practica-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/practica-is-alive/index.md","source":"@site/blog/practica-is-alive/index.md","title":"Practica.js v0.0.1 is alive","description":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.","date":"2022-07-15T10:00:00.000Z","formattedDate":"July 15, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"fastify","permalink":"/blog/tags/fastify"}],"readingTime":1.21,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"practica-is-alive","date":"2022-07-15T10:00","hide_table_of_contents":true,"title":"Practica.js v0.0.1 is alive","authors":["goldbergyoni"],"tags":["node.js","express","fastify"]},"prevItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.\\n\\n## What is Practica is one paragraph\\n\\nAlthough Node.js has great frameworks \ud83d\udc9a, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are [neatly and thoughtfully documented](./decisions/index). We strive to keep things as simple and standard as possible and base our work off the popular guide: [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices).\\n\\nYour developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you\'ll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app. \\n\\n## 90 seconds video\\n\\n\\n\\n## How to get started\\n\\nTo get up to speed quickly, read our [getting started guide](https://practica.dev/the-basics/getting-started-quickly)."}]}')}}]); \ No newline at end of file diff --git a/assets/js/b2f554cd.d06d85c3.js b/assets/js/b2f554cd.d06d85c3.js new file mode 100644 index 00000000..d9c12650 --- /dev/null +++ b/assets/js/b2f554cd.d06d85c3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1477],{10:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"testing-the-dark-scenarios-of-your-nodejs-application","metadata":{"permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md","source":"@site/blog/crucial-tests/index.md","title":"Testing the dark scenarios of your Node.js application","description":"Where the dead-bodies are covered","date":"2023-07-07T11:00:00.000Z","formattedDate":"July 7, 2023","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"testing","permalink":"/blog/tags/testing"},{"label":"component-test","permalink":"/blog/tags/component-test"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"unit-test","permalink":"/blog/tags/unit-test"},{"label":"integration","permalink":"/blog/tags/integration"},{"label":"nock","permalink":"/blog/tags/nock"}],"readingTime":19.875,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"}],"frontMatter":{"slug":"testing-the-dark-scenarios-of-your-nodejs-application","date":"2023-07-07T11:00","hide_table_of_contents":true,"title":"Testing the dark scenarios of your Node.js application","authors":["goldbergyoni","razluvaton"],"tags":["node.js","testing","component-test","fastify","unit-test","integration","nock"]},"nextItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"}},"content":"## Where the dead-bodies are covered\\n\\nThis post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked\\n\\nSome context first: How do we test a modern backend? With [the testing diamond](https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55), of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we\'ve also written [a guide with 50 best practices for integration tests in Node.js](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n\\nBut there is a pitfall: most developers write _only_ semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don\'t simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime\\n\\n![The hidden corners](./the-hidden-corners.png)\\n\\nHere are a handful of examples that might open your mind to a whole new class of risks and tests\\n\\n## \ud83e\udddf\u200d\u2640\ufe0f The zombie process test\\n\\n**\ud83d\udc49What & so what? -** In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see [readiness probe](https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod.)). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a \'zombie process\'. In this scenario, the runtime platform won\'t realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, api.js:**\\n\\n```javascript\\n// A common express server initialization\\nconst startWebServer = () => {\\n return new Promise((resolve, reject) => {\\n try {\\n // A typical Express setup\\n expressApp = express();\\n defineRoutes(expressApp); // a function that defines all routes\\n expressApp.listen(process.env.WEB_SERVER_PORT);\\n } catch (error) {\\n //log here, fire a metric, maybe even retry and finally:\\n process.exit();\\n }\\n });\\n};\\n```\\n\\n**The test:**\\n\\n```javascript\\nconst api = require(\'./entry-points/api\'); // our api starter that exposes \'startWebServer\' function\\nconst sinon = require(\'sinon\'); // a mocking library\\n\\ntest(\'When an error happens during the startup phase, then the process exits\', async () => {\\n // Arrange\\n const processExitListener = sinon.stub(process, \'exit\');\\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\\n sinon\\n .stub(routes, \'defineRoutes\')\\n .throws(new Error(\'Cant initialize connection\'));\\n\\n // Act\\n await api.startWebServer();\\n\\n // Assert\\n expect(processExitListener.called).toBe(true);\\n});\\n```\\n\\n## \ud83d\udc40 The observability test\\n\\n**\ud83d\udc49What & why -** For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error **correctly observable**. In plain words, ensuring that it\'s being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, _including stack trace_, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn\'t care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When exception is throw during request, Then logger reports the mandatory fields\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n status: \'approved\',\\n };\\n const metricsExporterDouble = sinon.stub(metricsExporter, \'fireMetric\');\\n sinon\\n .stub(OrderRepository.prototype, \'addOrder\')\\n .rejects(new AppError(\'saving-failed\', \'Order could not be saved\', 500));\\n const loggerDouble = sinon.stub(logger, \'error\');\\n\\n //Act\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n expect(loggerDouble).toHaveBeenCalledWith({\\n name: \'saving-failed\',\\n status: 500,\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n expect(\\n metricsExporterDouble).toHaveBeenCalledWith(\'error\', {\\n errorName: \'example-error\',\\n })\\n});\\n```\\n\\n## \ud83d\udc7d The \'unexpected visitor\' test - when an uncaught exception meets our code\\n\\n**\ud83d\udc49What & why -** A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let\'s focus on the 2nd assumption: it\'s common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on(\'error\', ...). To name a few examples. These errors will find their way to the global process.on(\'uncaughtException\') handler, **hopefully if your code subscribed**. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here\'s a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is \'borderless\', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here\'s an example:\\n\\nresearches says that, rejection\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When an unhandled exception is thrown, then process stays alive and the error is logged\', async () => {\\n //Arrange\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const processExitListener = sinon.stub(process, \'exit\');\\n const errorToThrow = new Error(\'An error that wont be caught \ud83d\ude33\');\\n\\n //Act\\n process.emit(\'uncaughtException\', errorToThrow); //\ud83d\udc48 Where the magic is\\n\\n // Assert\\n expect(processExitListener.called).toBe(false);\\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\\n});\\n```\\n\\n## \ud83d\udd75\ud83c\udffc The \'hidden effect\' test - when the code should not mutate at all\\n\\n**\ud83d\udc49What & so what -** In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn\'t have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn\'t guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you\'re not cleaning the DB often (like me, but that\'s another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\nit(\'When adding an invalid order, then it returns 400 and NOT retrievable\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n mode: \'draft\',\\n externalIdentifier: uuid(), //no existing record has this value\\n };\\n\\n //Act\\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\\n \'/order\',\\n orderToAdd\\n );\\n\\n //Assert\\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\\n ); // Trying to get the order that should have failed\\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\\n addingHTTPStatus: 400,\\n fetchingHTTPStatus: 404,\\n });\\n // \ud83d\udc46 Check that no such record exists\\n});\\n```\\n\\n## \ud83e\udde8 The \'overdoing\' test - when the code should mutate but it\'s doing too much\\n\\n**\ud83d\udc49What & why -** This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here\'s a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more \'control\' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When deleting an existing order, Then it should NOT be retrievable\', async () => {\\n // Arrange\\n const orderToDelete = {\\n userId: 1,\\n productId: 2,\\n };\\n const deletedOrder = (await axiosAPIClient.post(\'/order\', orderToDelete)).data\\n .id; // We will delete this soon\\n const orderNotToBeDeleted = orderToDelete;\\n const notDeletedOrder = (\\n await axiosAPIClient.post(\'/order\', orderNotToBeDeleted)\\n ).data.id; // We will not delete this\\n\\n // Act\\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\\n\\n // Assert\\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${deletedOrder}`\\n );\\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${notDeletedOrder}`\\n );\\n expect(getNotDeletedOrderStatus).toBe(200);\\n expect(getDeletedOrderStatus).toBe(404);\\n});\\n```\\n\\n## \ud83d\udd70 The \'slow collaborator\' test - when the other HTTP service times out\\n\\n**\ud83d\udc49What & why -** When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it\'s harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like [nock](https://github.com/nock/nock) or [wiremock](https://wiremock.org/). These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available **in production**, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can\'t wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other \'chaotic\' scenarios. Question left is how to simulate slow response without having slow tests? You may use [fake timers](https://sinonjs.org/releases/latest/fake-timers/) and trick the system into believing as few seconds passed in a single tick. If you\'re using [nock](https://github.com/nock/nock), it offers an interesting feature to simulate timeouts **quickly**: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\n// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\\ntest(\'When users service times out, then return 503 (option 1 with fake timers)\', async () => {\\n //Arrange\\n const clock = sinon.useFakeTimers();\\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\\n nock(`${config.userServiceURL}/user/`)\\n .get(\'/1\', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\\n .reply(200);\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n mode: \'approved\',\\n };\\n\\n //Act\\n // \ud83d\udc47try to add new order which should fail due to User service not available\\n const response = await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n // \ud83d\udc47At least our code does its best given this situation\\n expect(response.status).toBe(503);\\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\\n name: \'user-service-not-available\',\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n});\\n```\\n\\n## \ud83d\udc8a The \'poisoned message\' test - when the message consumer gets an invalid payload that might put it in stagnation\\n\\n**\ud83d\udc49What & so what -** When testing flows that start or end in a queue, I bet you\'re going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you\'re using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the \'poisoned message\'. To mitigate this risk, the tests\' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why\\n\\nWhen testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. [SQS demand 60 seconds](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html) to purge queues), to name a few challenges that you won\'t find when dealing with real DB\\n\\nHere is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By \'fake\' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like [this one for SQS](https://github.com/m-radzikowski/aws-sdk-client-mock) and you can code one **easily** yourself. No worries, I\'m not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):\\n\\n**\ud83d\udcdd Code**\\n\\n1. Create a fake message queue that does almost nothing but record calls, see full example here\\n\\n```javascript\\nclass FakeMessageQueueProvider extends EventEmitter {\\n // Implement here\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n}\\n```\\n\\n2. Make your message queue client accept real or fake provider\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n // Pass to it a fake or real message queue\\n constructor(customMessageQueueProvider) {}\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // Simple implementation can be found here:\\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\\n}\\n```\\n\\n3. Expose a convenient function that tells when certain calls where made\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // \ud83d\udc47\\n waitForEvent(eventName: \'publish\' | \'consume\' | \'acknowledge\' | \'reject\', howManyTimes: number) : Promise\\n}\\n```\\n\\n4. The test is now short, flat and expressive \ud83d\udc47\\n\\n```javascript\\nconst FakeMessageQueueProvider = require(\'./libs/fake-message-queue-provider\');\\nconst MessageQueueClient = require(\'./libs/message-queue-client\');\\nconst newOrderService = require(\'./domain/newOrderService\');\\n\\ntest(\'When a poisoned message arrives, then it is being rejected back\', async () => {\\n // Arrange\\n const messageWithInvalidSchema = { nonExistingProperty: \'invalid\u274c\' };\\n const messageQueueClient = new MessageQueueClient(\\n new FakeMessageQueueProvider()\\n );\\n // Subscribe to new messages and passing the handler function\\n messageQueueClient.consume(\'orders.new\', newOrderService.addOrder);\\n\\n // Act\\n await messageQueueClient.publish(\'orders.new\', messageWithInvalidSchema);\\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\\n\\n // Assert\\n await messageQueueClient.waitFor(\'reject\', { howManyTimes: 1 });\\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js)\\n\\n## \ud83d\udce6 Test the package as a consumer\\n\\n**\ud83d\udc49What & why -** When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user\'s computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts _that were built_. See the mismatch here? _after_ running the tests, the package files are transpiled (I\'m looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files\\n\\n**\ud83d\udcdd Code**\\n\\nConsider the following scenario, you\'re developing a library, and you wrote this code:\\n```js\\n// index.js\\nexport * from \'./calculate.js\';\\n\\n// calculate.js \ud83d\udc48\\nexport function calculate() {\\n return 1;\\n}\\n```\\n\\nThen some tests:\\n```js\\nimport { calculate } from \'./index.js\';\\n\\ntest(\'should return 1\', () => {\\n expect(calculate()).toBe(1);\\n})\\n\\n\u2705 All tests pass \ud83c\udf8a\\n```\\n\\nFinally configure the package.json:\\n```json5\\n{\\n // ....\\n \\"files\\": [\\n \\"index.js\\"\\n ]\\n}\\n```\\n\\nSee, 100% coverage, all tests pass locally and in the CI \u2705, it just won\'t work in production \ud83d\udc79. Why? because you forgot to include the `calculate.js` in the package.json `files` array \ud83d\udc46\\n\\n\\nWhat can we do instead? we can test the library as _its end-users_. How? publish the package to a local registry like [verdaccio](https://verdaccio.org/), let the tests install and approach the *published* code. Sounds troublesome? judge yourself \ud83d\udc47\\n\\n**\ud83d\udcdd Code**\\n\\n```js\\n// global-setup.js\\n\\n// 1. Setup the in-memory NPM registry, one function that\'s it! \ud83d\udd25\\nawait setupVerdaccio();\\n\\n// 2. Building our package \\nawait exec(\'npm\', [\'run\', \'build\'], {\\n cwd: packagePath,\\n});\\n\\n// 3. Publish it to the in-memory registry\\nawait exec(\'npm\', [\'publish\', \'--registry=http://localhost:4873\'], {\\n cwd: packagePath,\\n});\\n\\n// 4. Installing it in the consumer directory\\nawait exec(\'npm\', [\'install\', \'my-package\', \'--registry=http://localhost:4873\'], {\\n cwd: consumerPath,\\n});\\n\\n// Test file in the consumerPath\\n\\n// 5. Test the package \ud83d\ude80\\ntest(\\"should succeed\\", async () => {\\n const { fn1 } = await import(\'my-package\');\\n\\n expect(fn1()).toEqual(1);\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/rluvaton/e2e-verdaccio-example)\\n\\nWhat else this technique can be useful for?\\n\\n- Testing different version of peer dependency you support - let\'s say your package support react 16 to 18, you can now test that\\n- You want to test ESM and CJS consumers\\n- If you have CLI application you can test it like your users\\n- Making sure all the voodoo magic in that babel file is working as expected\\n\\n## \ud83d\uddde The \'broken contract\' test - when the code is great but its corresponding OpenAPI docs leads to a production bug\\n\\n**\ud83d\udc49What & so what -** Quite confidently I\'m sure that almost no team test their OpenAPI correctness. \\"It\'s just documentation\\", \\"we generate it automatically based on code\\" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.\\n\\nConsider the following scenario, you\'re requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don\'t forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the \'contract\' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., [PACT](https://pact.io)), there are also leaner approaches that gets you covered _easily and quickly_ (at the price of covering less risks).\\n\\nThe following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It\'s a pity that these libs can\'t assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, an API throw a new error status**\\n\\n```javascript\\nif (doesOrderCouponAlreadyExist) {\\n throw new AppError(\'duplicated-coupon\', { httpStatus: 409 });\\n}\\n```\\n\\nThe OpenAPI doesn\'t document HTTP status \'409\', no framework knows to update the OpenAPI doc based on thrown exceptions\\n\\n```json\\n\\"responses\\": {\\n \\"200\\": {\\n \\"description\\": \\"successful\\",\\n }\\n ,\\n \\"400\\": {\\n \\"description\\": \\"Invalid ID\\",\\n \\"content\\": {}\\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\\n}\\n\\n```\\n\\n**The test code**\\n\\n```javascript\\nconst jestOpenAPI = require(\'jest-openapi\');\\njestOpenAPI(\'../openapi.json\');\\n\\ntest(\'When an order with duplicated coupon is added , then 409 error should get returned\', async () => {\\n // Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n couponId: uuid(),\\n };\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n // Act\\n // We\'re adding the same coupon twice \ud83d\udc47\\n const receivedResponse = await axios.post(\'/order\', orderToAdd);\\n\\n // Assert;\\n expect(receivedResponse.status).toBe(409);\\n expect(res).toSatisfyApiSpec();\\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\\n});\\n```\\n\\nTrick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in \'beforeAll\'. This covers all the tests against OpenAPI mismatches\\n\\n```javascript\\nbeforeAll(() => {\\n axios.interceptors.response.use((response) => {\\n expect(response.toSatisfyApiSpec());\\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\\n });\\n});\\n```\\n\\n## Even more ideas\\n\\n- Test readiness and health routes\\n- Test message queue connection failures\\n- Test JWT and JWKS failures\\n- Test security-related things like CSRF tokens\\n- Test your HTTP client retry mechanism (very easy with nock)\\n- Test that the DB migration succeed and the new code can work with old records format\\n- Test DB connection disconnects\\n \\n## It\'s not just ideas, it a whole new mindset\\n\\nThe examples above were not meant only to be a checklist of \'don\'t forget\' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this \'production-oriented development\'"},{"id":"monorepo-backend","metadata":{"permalink":"/blog/monorepo-backend","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md","source":"@site/blog/which-monorepo/index.md","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","description":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we\'d like to share our considerations in choosing our monorepo tooling","date":"2023-07-07T05:38:19.000Z","formattedDate":"July 7, 2023","tags":[{"label":"monorepo","permalink":"/blog/tags/monorepo"},{"label":"decisions","permalink":"/blog/tags/decisions"}],"readingTime":16.925,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"monorepo-backend","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","authors":["goldbergyoni","michaelsalomon"],"tags":["monorepo","decisions"]},"prevItem":{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},"nextItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"}},"content":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in [Practica.js](https://github.com/practicajs/practica). In this post, we\'d like to share our considerations in choosing our monorepo tooling\\n\\n![Monorepos](./monorepo-high-level.png)\\n\\n## What are we looking\xa0at\\n\\n\\nThe Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 [Lerna- has just retired.](https://github.com/lerna/lerna/issues/2703) When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.\\n\\nThis post is concerned with backend-only and Node.js. It also scoped to _typical_ business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like [Bazel](https://github.com/thundergolfer/example-bazel-monorepo) is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison).\\n\\nLet\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following _4 layers below._ Each one of them can bring value to your project, each has different consequences, tooling, and features:\\n\\n\x3c!--truncate--\x3e\\n\\n\\n# Layer 1: Plain old folders to stay on top of your code\\n\\nWith zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, _quickly_ adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.\\n\\nThis layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling.\\n\\nWith that said, some of the newer tools augment this experience with interesting features:\\n\\n- Both [Turborepo](https://turborepo.org/) and [Nx](https://nx.dev/structure/dependency-graph) and also [Lerna](https://www.npmjs.com/package/lerna-dependency-graph) provide a visual representation of the packages\u2019 dependencies\\n- [Nx allows \u2018visibility rules\u2019](https://nx.dev/structure/monorepo-tags) which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)\\n\\n![](https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg)\\n\\nNx dependencies graph\\n\\n- [Nx workspace generator](https://nx.dev/generators/workspace-generators) allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing\\n\\n# Layer 2: Tasks and pipeline to build your code efficiently\\n\\nEven in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of _multiple_ components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.\\n\\n![](https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png)\\n\\nApply some commands over multiple packages\\n\\nIn some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 _deeply_ optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:\\n\\n- **Parallelization \u2014** If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?\\n- **Smart execution plan \u2014**Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s _isolated_ unit tests _while_ building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement\\n\\n![](https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png)\\n\\nA modern tool advantage over old Lerna. Taken from Turborepo website\\n\\n- **Detect who is affected by a change \u2014** Even on a system with high coupling between packages, it\u2019s usually not necessary to run _all_ packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed\\n- **Sub-systems (i.e., projects) \u2014** Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group\\n- **Caching \u2014** This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!\\n- **Remote caching \u2014** Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time\\n\\n# Layer 3: Hoist your dependencies to boost npm installation\\n\\nThe speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate ([the NPM doppelgangers problem](https://rushjs.io/pages/advanced/npm_doppelgangers/)) the installation of those packages which might result in a long process.\\n\\nThis is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.\\n\\nBoth Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.\\n\\n![](https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png)\\n\\nOn top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default.\\n\\nI\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js _runtime_ and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019.\\n\\n# Stop for a second: It\u2019s all about your workflow\\n\\nWe deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is _synchronized or independent_ (we will discuss this in a few seconds)_._ This upfront _fundamental_ decision will change almost everything.\\n\\nConsider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?\\n\\n**Option A \u2014 The synchronized workflow-** Going with this development style, all the three components will be developed and deployed in one chunk _together_. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they\'re ready, the version of all components will get bumped. Finally, they will get deployed _together._\\n\\nGoing with this approach, the developer has the chance of seeing the full flow from the client\'s perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.\\n\\n**Option B \u2014 Independent workflow-** This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it.\\n\\nGoing with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also _forces her_ to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious.\\n\\n![](https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png)\\n\\nSynchronized and independent workflows illustrated\\n\\n**On the illusion of synchronicity**\\n\\nIn distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.\\n\\nThis fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph.\\n\\n# Layer 4: Link your packages for immediate feedback\\n\\nWhen having a Monorepo, there is always the unavoidable dilemma of how to link between the components:\\n\\n**Option 1: Using npm \u2014** Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.\\n\\n**Option2: Just a plain folder \u2014** With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented.\\n\\nWith option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes.\\n\\nWith option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.\\n\\nHow do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry.\\n\\n![](https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png)\\n\\nLinking packages in a Monorepo\\n\\nIf you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.\\n\\nIf favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is _temporarily link_ (with npm link) between the packages_,_ debug, code, then finally remove the link.\\n\\nNx is taking a slightly more disruptive approach \u2014 it is using [TypeScript paths](https://www.typescriptlang.org/tsconfig#paths) to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.\\n\\n# Closing: What should you use?\\n\\nIt\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision.\\n\\n**Scenario A \u2014** If your architecture dictates a _synchronized workflow_ where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.\\n\\nFor example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many _relatively_ coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.\\n\\nIf your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.\\n\\n![](https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png)\\n\\n**Scenario B\u2014** If you\u2019re into an _independent workflow_ where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is [Bilt](https://github.com/giltayar/bilt) by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.\\n\\n**In any scenario, consider workspaces \u2014** If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain.\\n\\nWe tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.\\n\\n# Bonus: Comparison table\\n\\nSee below a detailed comparison table of the various tools and features:\\n\\n![](https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png)\\n\\nPreview only, the complete table can be [found here](https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md)"},{"id":"practica-v0.0.6-is-alive","metadata":{"permalink":"/blog/practica-v0.0.6-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md","source":"@site/blog/v0.6-is-alive/index.md","title":"Practica v0.0.6 is alive","description":"Where is our focus now?","date":"2022-12-10T10:00:00.000Z","formattedDate":"December 10, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"prisma","permalink":"/blog/tags/prisma"}],"readingTime":1.47,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"},{"name":"Daniel Gluskin","title":"Practica.js core maintainer","url":"https://github.com/DanielGluskin","imageURL":"https://avatars.githubusercontent.com/u/17989958?v=4","key":"danielgluskin"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"practica-v0.0.6-is-alive","date":"2022-12-10T10:00","hide_table_of_contents":true,"title":"Practica v0.0.6 is alive","authors":["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],"tags":["node.js","express","practica","prisma"]},"prevItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"},"nextItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"}},"content":"## Where is our focus now?\\n\\nWe work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback\\n\\n## What\'s new?\\n\\n### Request-level store\\n\\nEvery request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is \'request-id\' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in [AsyncLocal](https://nodejs.org/api/async_context.html) for this task\\n\\n### Hardened .dockerfile\\n\\nAlthough a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from [this article](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/) and already apply 90% of the guidelines\\n\\n### Additional ORM option: Prisma\\n\\nPrisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma\\n\\nWhy did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this [blog post](https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/)\\n\\n### Many small enhancements\\n\\nMore than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more\\n\\n## Where do I start?\\n\\nDefinitely follow the [getting started guide first](https://practica.dev/the-basics/getting-started-quickly) and then read the guide [coding with practica](https://practica.dev/the-basics/coding-with-practica) to realize its full power and genuine value. We will be thankful to receive your feedback"},{"id":"is-prisma-better-than-your-traditional-orm","metadata":{"permalink":"/blog/is-prisma-better-than-your-traditional-orm","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/is-prisma-better/index.md","source":"@site/blog/is-prisma-better/index.md","title":"Is Prisma better than your \'traditional\' ORM?","description":"Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?","date":"2022-12-07T11:00:00.000Z","formattedDate":"December 7, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":23.875,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"is-prisma-better-than-your-traditional-orm","date":"2022-12-07T11:00","hide_table_of_contents":true,"title":"Is Prisma better than your \'traditional\' ORM?","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"},"nextItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"## Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?\\n\\n*Betteridge\'s law of headlines suggests that a \'headline that ends in a question mark can be answered by the word NO\'. Will this article follow this rule?*\\n\\nImagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it\'s hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained\\n\\n![Suite with stain](./suite.png)\\n\\n Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, \\"I wish we had something like (Java) hibernate or (.NET) Entity Framework\\" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don\'t feel delightful, some may say even mediocre. At least so I believed *before* writing this article...\\n\\nFrom time to time, a shiny new ORM is launched, and there is hope. Then soon it\'s realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It\'s gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the \'Ferrari\' ORM we\'ve been waiting for? Is it a game changer? If you\'re are the \'no ORM for me\' type, will this one make you convert your religion?\\n\\nIn [Practica.js](https://github.com/practicajs/practica) (the Node.js starter based off [Node.js best practices with 83,000 stars](https://github.com/goldbergyoni/nodebestpractices)) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?\\n\\nThis article is certainly not an \'ORM 101\' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It\'s compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren\'t covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs\\n\\nReady to explore how good Prisma is and whether you should throw away your current tools?\\n\\n\x3c!--truncate--\x3e\\n\\n## TOC\\n\\n1. Prisma basics in 3 minutes\\n2. Things that are mostly the same\\n3. Differentiation\\n4. Closing\\n\\n## Prisma basics in 3 minutes\\n\\nJust before delving into the strategic differences, for the benefit of those unfamiliar with Prisma - here is a quick \'hello-world\' workflow with Prisma ORM. If you\'re already familiar with it - skipping to the next section sounds sensible. Simply put, Prisma dictates 3 key steps to get our ORM code working:\\n\\n**A. Define a model -** Unlike almost any other ORM, Prisma brings a unique language (DSL) for modeling the database-to-code mapping. This proprietary syntax aims to express these models with minimum clutter (i.e., TypeScript generics and verbose code). Worried about having intellisense and validation? A well-crafted vscode extension gets you covered. In the following example, the prisma.schema file describes a DB with an Order table that has a one-to-many relation with a Country table:\\n\\n```prisma\\n// prisma.schema file\\nmodel Order {\\n id Int @id @default(autoincrement())\\n userId Int?\\n paymentTermsInDays Int?\\n deliveryAddress String? @db.VarChar(255)\\n country Country @relation(fields: [countryId], references: [id])\\n countryId Int\\n}\\n\\nmodel Country {\\n id Int @id @default(autoincrement())\\n name String @db.VarChar(255)\\n Order Order[]\\n}\\n```\\n\\n**B. Generate the client code -** Another unusual technique: to get the ORM code ready, one must invoke Prisma\'s CLI and ask for it: \\n\\n```bash\\nnpx prisma generate\\n```\\n\\nAlternatively, if you wish to have your DB ready and the code generated with one command, just fire:\\n\\n```bash\\nnpx prisma migrate deploy\\n```\\n\\nThis will generate migration files that you can execute later in production and also the ORM client code\\n\\n\\nThis will generate migration files that you can execute later in production and the TypeScript ORM code based on the model. The generated code location is defaulted under \'[root]/NODE_MODULES/.prisma/client\'. Every time the model changes, the code must get re-generated again. While most ORMs name this code \'repository\' or \'entity\' or \'active record\', interestingly, Prisma calls it a \'client\'. This shows part of its unique philosophy, which we will explore later\\n\\n**C. All good, use the client to interact with the DB -** The generated client has a rich set of functions and types for your DB interactions. Just import the ORM/client code and use it:\\n\\n```javascript\\nimport { PrismaClient } from \'.prisma/client\';\\n\\nconst prisma = new PrismaClient();\\n// A query example\\nawait prisma.order.findMany({\\n where: {\\n paymentTermsInDays: 30,\\n },\\n orderBy: {\\n id: \'asc\',\\n },\\n });\\n// Use the same client for insertion, deletion, updates, etc\\n```\\n\\nThat\'s the nuts and bolts of Prisma. Is it different and better?\\n\\n## What is the same?\\n\\nWhen comparing options, before outlining differences, it\'s useful to state what is actually similar among these products. Here is a partial list of features that both TypeORM, Sequelize and Prisma support\\n\\n- Casual queries with sorting, filtering, distinct, group by, \'upsert\' (update or create),etc\\n- Raw queries\\n- Full text search\\n- Association/relations of any type (e.g., many to many, self-relation, etc)\\n- Aggregation queries\\n- Pagination\\n- CLI\\n- Transactions\\n- Migration & seeding\\n- Hooks/events (called middleware in Prisma)\\n- Connection pool\\n- Based on various community benchmarks, no dramatic performance differences\\n- All have huge amount of stars and downloads\\n\\nOverall, I found TypeORM and Sequelize to be a little more feature rich. For example, the following features are not supported only in Prisma: GIS queries, DB-level custom constraints, DB replication, soft delete, caching, exclude queries and some more\\n\\nWith that, shall we focus on what really set them apart and make a difference\\n\\n## What is fundamentally different?\\n\\n### 1. Type safety across the board\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** ORM\'s life is not easier since the TypeScript rise, to say the least. The need to support typed models/queries/etc yields a lot of developers sweat. Sequelize, for example, struggles to stabilize a TypeScript interface and, by now offers 3 different syntaxes + one external library ([sequelize-typescript](https://github.com/sequelize/sequelize-typescript)) that offers yet another style. Look at the syntax below, this feels like an afterthought - a library that was not built for TypeScript and now tries to squeeze it in somehow. Despite the major investment, both Sequelize and TypeORM offer only partial type safety. Simple queries do return typed objects, but other common corner cases like attributes/projections leave you with brittle strings. Here are a few examples:\\n\\n\\n```javascript\\n// Sequelize pesky TypeScript interface\\ntype OrderAttributes = {\\n id: number,\\n price: number,\\n // other attributes...\\n};\\n\\ntype OrderCreationAttributes = Optional;\\n\\n//\ud83d\ude2f Isn\'t this a weird syntax?\\nclass Order extends Model, InferCreationAttributes> {\\n declare id: CreationOptional;\\n declare price: number;\\n}\\n```\\n\\n```javascript\\n// Sequelize loose query types\\nawait getOrderModel().findAll({\\n where: { noneExistingField: \'noneExistingValue\' } //\ud83d\udc4d TypeScript will warn here\\n attributes: [\'none-existing-field\', \'another-imaginary-column\'], // No errors here although these columns do not exist\\n include: \'no-such-table\', //\ud83d\ude2f no errors here although this table doesn\'t exist\\n });\\n await getCountryModel().findByPk(\'price\'); //\ud83d\ude2f No errors here although the price column is not a primary key\\n```\\n\\n```javascript\\n// TypeORM loose query\\nconst ordersOnSales: Post[] = await orderRepository.find({\\n where: { onSale: true }, //\ud83d\udc4d TypeScript will warn here\\n select: [\'id\', \'price\'],\\n})\\nconsole.log(ordersOnSales[0].userId); //\ud83d\ude2f No errors here although the \'userId\' column is not part of the returned object\\n```\\n\\nIsn\'t it ironic that a library called **Type**ORM base its queries on strings?\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It takes a totally different approach by generating per-project client code that is fully typed. This client embodies types for everything: every query, relations, sub-queries, everything (except migrations). While other ORMs struggles to infer types from discrete models (including associations that are declared in other files), Prisma\'s offline code generation is easier: It can look through the entire DB relations, use custom generation code and build an almost perfect TypeScript experience. Why \'almost\' perfect? for some reason, Prisma advocates using plain SQL for migrations, which might result in a discrepancy between the code models and the DB schema. Other than that, this is how Prisma\'s client brings end to end type safety:\\n\\n```javascript\\nawait prisma.order.findMany({\\n where: {\\n noneExistingField: 1, //\ud83d\udc4d TypeScript error here\\n },\\n select: {\\n noneExistingRelation: { //\ud83d\udc4d TypeScript error here\\n select: { id: true }, \\n },\\n noneExistingField: true, //\ud83d\udc4d TypeScript error here\\n },\\n });\\n\\n await prisma.order.findUnique({\\n where: { price: 50 }, //\ud83d\udc4d TypeScript error here\\n });\\n```\\n\\n**\ud83d\udcca How important:** TypeScript support across the board is valuable for DX mostly. Luckily, we have another safety net: The project testing. Since tests are mandatory, having build-time type verification is important but not a life saver\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n## 2. Make you forget SQL\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Many avoid ORMs while preferring to interact with the DB using lower-level techniques. One of their arguments is against the efficiency of ORMs: Since the generated queries are not visible immediately to the developers, wasteful queries might get executed unknowingly. While all ORMs provide syntactic sugar over SQL, there are subtle differences in the level of abstraction. The more the ORM syntax resembles SQL, the more likely the developers will understand their own actions\\n\\nFor example, TypeORM\'s query builder looks like SQL broken into convenient functions\\n\\n```javascript\\nawait createQueryBuilder(\'order\')\\n .leftJoinAndSelect(\\n \'order.userId\',\\n \'order.productId\',\\n \'country.name\',\\n \'country.id\'\\n )\\n .getMany();\\n```\\n\\nA developer who read this code \ud83d\udc46 is likely to infer that a *join* query between two tables will get executed\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma\'s mission statement is to simplify DB work, the following statement is taken from their homepage:\\n\\n\\"We designed its API to be intuitive, both for SQL veterans and *developers brand new to databases*\\"\\n\\nBeing ambitious to appeal also to database layman, Prisma builds a syntax with a little bit higher abstraction, for example:\\n\\n```javascript\\nawait prisma.order.findMany({\\n select: {\\n userId: true,\\n productId: true,\\n country: {\\n select: { name: true, id: true },\\n },\\n },\\n});\\n\\n```\\n\\nNo join is reminded here also it fetches records from two related tables (order, and country). Could you guess what SQL is being produced here? how many queries? One right, a simple join? Surprise, actually, two queries are made. Prisma fires one query per-table here, as the join logic happens on the ORM client side (not inside the DB). But why?? in some cases, mostly where there is a lot of repetition in the DB cartesian join, querying each side of the relation is more efficient. But in other cases, it\'s not. Prisma arbitrarily chose what they believe will perform better in *most* cases. I checked, in my case it\'s *slower* than doing a one-join query on the DB side. As a developer, I would miss this deficiency due to the high-level syntax (no join is mentioned). My point is, Prisma sweet and simple syntax might be a bless for developer who are brand new to databases and aim to achieve a working solution in a short time. For the longer term, having full awareness of the DB interactions is helpful, other ORMs encourage this awareness a little better\\n\\n**\ud83d\udcca How important:** Any ORM will hide SQL details from their users - without developer\'s awareness no ORM will save the day\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Not necessarily\\n\\n## 3. Performance\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Speak to an ORM antagonist and you\'ll hear a common sensible argument: ORMs are much slower than a \'raw\' approach. To an extent, this is a legit observation as [most comparisons](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802) will show none-negligible differences between raw/query-builder and ORM.\\n\\n![raw is faster d](./pg-driver-is-faster.png)\\n*Example: a direct insert against the PG driver is much shorter [Source](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802)* \\n\\n It should also be noted that these benchmarks don\'t tell the entire story - on top of raw queries, every solution must build a mapper layer that maps the raw data to JS objects, nest the results, cast types, and more. This work is included within every ORM but not shown in benchmarks for the raw option. In reality, every team which doesn\'t use ORM would have to build their own small \\"ORM\\", including a mapper, which will also impact performance\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It was my hope to see a magic here, eating the ORM cake without counting the calories, seeing Prisma achieving an almost \'raw\' query speed. I had some good and logical reasons for this hope: Prisma uses a DB client built with Rust. Theoretically, it could serialize to and nest objects faster (in reality, this happens on the JS side). It was also built from the ground up and could build on the knowledge pilled in ORM space for years. Also, since it returns POJOs only (see bullet \'No Active Record here!\') - no time should be spent on decorating objects with ORM fields\\n\\nYou already got it, this hope was not fulfilled. Going with every community benchmark ([one](https://dev.to/josethz00/benchmark-prisma-vs-typeorm-3873), [two](https://github.com/edgedb/imdbench), [three](https://deepkit.io/library)), Prisma at best is not faster than the average ORM. What is the reason? I can\'t tell exactly but it might be due the complicated system that must support Go, future languages, MongoDB and other non-relational DBs\\n\\n![Prisma is not faster](./throughput-benchmark.png)\\n*Example: Prisma is not faster than others. It should be noted that in other benchmarks Prisma scores higher and shows an \'average\' performance [Source](https://github.com/edgedb/imdbench)*\\n\\n**\ud83d\udcca How important:** It\'s expected from ORM users to live peacefully with inferior performance, for many systems it won\'t make a great deal. With that, 10%-30% performance differences between various ORMs are not a key factor\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** No\\n\\n## 4. No active records here!\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Node in its early days was heavily inspired by Ruby (e.g., testing \\"describe\\"), many great patterns were embraced, [Active Record](https://en.wikipedia.org/wiki/Active_record_pattern) is not among the successful ones. What is this pattern about in a nutshell? say you deal with Orders in your system, with Active Record an Order object/class will hold both the entity properties, possible also some of the logic functions and also CRUD functions. Many find this pattern to be awful, why? ideally, when coding some logic/flow, one should not keep her mind busy with side effects and DB narratives. It also might be that accessing some property unconsciously invokes a heavy DB call (i.e., lazy loading). If not enough, in case of heavy logic, unit tests might be in order (i.e., read [\'selective unit tests\'](https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/)) - it\'s going to be much harder to write unit tests against code that interacts with the DB. In fact, all of the respectable and popular architecture (e.g., DDD, clean, 3-tiers, etc) advocate to \'isolate the domain\', separate the core/logic of the system from the surrounding technologies. With all of that said, both TypeORM and Sequelize support the Active Record pattern which is displayed in many examples within their documentation. Both also support other better patterns like the data mapper (see below), but they still open the door for doubtful patterns\\n\\n\\n```javascript\\n// TypeORM active records \ud83d\ude1f\\n\\n@Entity()\\nclass Order extends BaseEntity {\\n @PrimaryGeneratedColumn()\\n id: number\\n\\n @Column()\\n price: number\\n\\n @ManyToOne(() => Product, (product) => product.order)\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n // some logic here\\n orderToUpdate.status = \\"approval\\";\\n orderToUpdate.save(); \\n orderToUpdate.products.forEach((products) =>{ \\n\\n })\\n orderToUpdate.usedConnection = ? \\n }\\n}\\n\\n\\n\\n```\\n\\n**\ud83e\udd14 How Prisma is different:** The better alternative is the data mapper pattern. It acts as a bridge, an adapter, between simple object notations (domain objects with properties) to the DB language, typically SQL. Call it with a plain JS object, POJO, get it saved in the DB. Simple. It won\'t add functions to the result objects or do anything beyond returning pure data, no surprising side effects. In its purest sense, this is a DB-related utility and completely detached from the business logic. While both Sequelize and TypeORM support this, Prisma offers *only* this style - no room for mistakes.\\n\\n\\n```javascript\\n// Prisma approach with a data mapper \ud83d\udc4d\\n\\n// This was generated automatically by Prisma\\ntype Order {\\n id: number\\n\\n price: number\\n\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n orderToUpdate.status = \\"approval\\";\\n prisma.order.update({ where: { id: orderToUpdate.id }, data: orderToUpdate }); \\n // Side effect \ud83d\udc46, but an explicit one. The thoughtful coder will move this to another function. Since it\'s happening outside, mocking is possible \ud83d\udc4d\\n products.forEach((products) =>{ // No lazy loading, the data is already here \ud83d\udc4d\\n\\n })\\n } \\n}\\n```\\n\\n In [Practica.js](https://github.com/practicajs/practica) we take it one step further and put the prisma models within the \\"DAL\\" layer and wrap it with the [repository pattern](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design). You may glimpse [into the code here](https://github.com/practicajs/practica/blob/21ff12ba19cceed9a3735c09d48184b5beb5c410/src/code-templates/services/order-service/domain/new-order-use-case.ts#L21), this is the business flow that calls the DAL layer\\n\\n\\n**\ud83d\udcca How important:** On the one hand, this is a key architectural principle to follow but the other hand most ORMs *allow* doing it right\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Yes!\\n\\n## 5. Documentation and developer-experience\\n\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** TypeORM and Sequelize documentation is mediocre, though TypeORM is a little better. Based on my personal experience they do get a little better over the years, but still by no mean they deserve to be called \\"good\\" or \\"great\\". For example, if you seek to learn about \'raw queries\' - Sequelize offers [a very short page](https://sequelize.org/docs/v6/core-concepts/raw-queries/) on this matter, TypeORM info is spread in multiple other pages. Looking to learn about pagination? Couldn\'t find Sequelize documents, TypeORM has [some short explanation](https://typeorm.io/select-query-builder#using-pagination), 150 words only\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma documentation rocks! See their documents on similar topics: [raw queries](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access) and [pagingation](https://www.prisma.io/docs/concepts/components/prisma-client/pagination), thousands of words, and dozens of code examples. The writing itself is also great, feels like some professional writers were involved\\n\\n![Prisma docs are comprehensive](./count-docs.png)\\n \\nThis chart above shows how comprehensive are Prisma docs (Obviously this by itself doesn\'t prove quality)\\n\\n**\ud83d\udcca How important:** Great docs are a key to awareness and avoiding pitfalls\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** You bet\\n\\n## 6. Observability, metrics, and tracing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Good chances are (say about 99.9%) that you\'ll find yourself diagnostic slow queries in production or any other DB-related quirks. What can you expect from traditional ORMs in terms of observability? Mostly logging. [Sequelize provides both logging](https://sequelize.org/api/v7/interfaces/queryoptions#benchmark) of query duration and programmatic access to the connection pool state ({size,available,using,waiting}). [TypeORM provides only logging](https://orkhan.gitbook.io/typeorm/docs/logging) of queries that suppress a pre-defined duration threshold. This is better than nothing, but assuming you don\'t read production logs 24/7, you\'d probably need more than logging - an alert to fire when things seem faulty. To achieve this, it\'s your responsibility to bridge this info to your preferred monitoring system. Another logging downside for this sake is verbosity - we need to emit tons of information to the logs when all we really care for is the average duration. Metrics can serve this purpose much better as we\'re about to see soon with Prisma\\n\\nWhat if you need to dig into which specific part of the query is slow? unfortunately, there is no breakdown of the query phases duration - it\'s being left to you as a black-box\\n\\n```javascript\\n// Sequelize - logging various DB information\\n\\n```\\n\\n![Logging query duration](./sequelize-log.png)\\nLogging each query in order to realize trends and anomaly in the monitoring system\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Since Prisma targets also enterprises, it must bring strong ops capabilities. Beautifully, it packs support for both [metrics](https://www.prisma.io/docs/concepts/components/prisma-client/metrics) and [open telemetry tracing](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing)!. For metrics, it generates custom JSON with metric keys and values so anyone can adapt this to any monitoring system (e.g., CloudWatch, statsD, etc). On top of this, it produces out of the box metrics in [Prometheus](https://prometheus.io/) format (one of the most popular monitoring platforms). For example, the metric \'prisma_client_queries_duration_histogram_ms\' provides the average query length in the system overtime. What is even more impressive is the support for open-tracing - it feeds your OpenTelemetry collector with spans that describe the various phases of every query. For example, it might help realize what is the bottleneck in the query pipeline: Is it the DB connection, the query itself or the serialization?\\n\\n![prisma tracing](./trace-diagram.png)\\nPrisma visualizes the various query phases duration with open-telemtry \\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n\\n**\ud83d\udcca How important:** Goes without words how impactful is observability, however filling the gap in other ORM will demand no more than a few days\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n## 7. Continuity - will it be here with us in 2024/2025\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** We live quite peacefully with the risk of one of our dependencies to disappear. With ORM though, this risk demand special attention because our buy-in is higher (i.e., harder to replace) and maintaining it was proven to be harder. Just look at a handful of successful ORMs in the past: objection.js, waterline, bookshelf - all of these respectful project had 0 commits in the past month. The single maintainer of objection.js [announced that he won\'t work the project anymore](https://github.com/Vincit/objection.js/issues/2335). This high churn rate is not surprising given the huge amount of moving parts to maintain, the gazillion corner cases and the modest \'budget\' OSS projects live with. Looking at OpenCollective shows that [Sequelize](https://opencollective.com/sequelize#category-BUDGET) and [TypeORM](https://opencollective.com/typeorm) are funded with ~1500$ month in average. This is barely enough to cover a daily Starbucks cappuccino and croissant (6.95$ x 365) for 5 maintainers. Nothing contrasts this model more than a startup company that just raised its series B - Prisma is [funded with 40,000,000$ (40 millions)](https://www.prisma.io/blog/series-b-announcement-v8t12ksi6x#:~:text=We%20are%20excited%20to%20announce,teams%20%26%20organizations%20in%20this%20article.) and recruited 80 people! Should not this inspire us with high confidence about their continuity? I\'ll surprisingly suggest that quite the opposite is true\\n\\nSee, an OSS ORM has to go over one huge hump, but a startup company must pass through TWO. The OSS project will struggle to achieve the critical mass of features, including some high technical barriers (e.g., TypeScript support, ESM). This typically lasts years, but once it does - a project can focus mostly on maintenance and step out of the danger zone. The good news for TypeORM and Sequelize is that they already did! Both struggled to keep their heads above the water, there were rumors in the past that [TypeORM is not maintained anymore](https://github.com/typeorm/typeorm/issues/3267), but they managed to go through this hump. I counted, both projects had approximately ~2000 PRs in the past 3 years! Going with [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize), each see multiple commits every week. They both have vibrant traction, and the majority of features you would expect from an ORM. TypeORM even supports beyond-the-basics features like multi data source and caching. It\'s unlikely that now, once they reached the promise land - they will fade away. It might happen, there is no guarantee in the OSS galaxy, but the risk is low\\n\\n![One hump](./one-hump.png)\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma a little lags behind in terms of features, but with a budget of 40M$ - there are good reasons to believe that they will pass the first hump, achieving a critical mass of features. I\'m more concerned with the second hump - showing revenues in 2 years or saying goodbye. As a company that is backed by venture capitals - the model is clear and cruel: In order to secure their next round, series B or C (depends whether the seed is counted), there must be a viable and proven business model. How do you \'sell\' ORM? Prisma experiments with multiple products, none is mature yet or being paid for. How big is this risk? According to [this startup companies success statistics](https://spdload.com/blog/startup-success-rate/), \\"About 65% of the Series A startups get series B, while 35% of the companies that get series A fail.\\". Since Prisma already gained a lot of love and adoption from the community, there success chances are higher than the average round A/B company, but even 20% or 10% chances to fade away is concerning\\n\\n> This is terrifying news - companies happily choose a young commercial OSS product without realizing that there are 10-30% chances for this product to disappear\\n\\n\\n![Two humps](./two-humps.png)\\n\\nSome of startup companies who seek a viable business model do not shut the doors rather change the product, the license or the free features. This is not my subjective business analysis, here are few examples: [MongoDB changed their license](https://techcrunch.com/2018/10/16/mongodb-switches-up-its-open-source-license/), this is why the majority had to host their Mongo DB over a single vendor. [Redis did something similar](https://techcrunch.com/2019/02/21/redis-labs-changes-its-open-source-license-again/). What are the chances of Prisma pivoting to another type of product? It actually already happened before, Prisma 1 was mostly about graphQL client and server, [it\'s now retired](https://github.com/prisma/prisma1)\\n\\nIt\'s just fair to mention the other potential path - most round B companies do succeed to qualify for the next round, when this happens even bigger money will be involved in building the \'Ferrari\' of JavaScript ORMs. I\'m surely crossing my fingers for these great people, at the same time we have to be conscious about our choices\\n\\n**\ud83d\udcca How important:** As important as having to code again the entire DB layer in a big system\\n\\n![Medium importance](./high2-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Quite the opposite\\n\\n## Closing - what should you use now?\\n\\nBefore proposing my key take away - which is the primary ORM, let\'s repeat the key learning that were introduced here:\\n\\n1. \ud83e\udd47 Prisma deserves a medal for its awesome DX, documentation, observability support and end-to-end TypeScript coverage\\n2. \ud83e\udd14 There are reasons to be concerned about Prisma\'s business continuity as a young startup without a viable business model. Also Prisma\'s abstract client syntax might blind developers a little more than other ORMs\\n3. \ud83c\udfa9 The contenders, TypeORM and Sequelize, matured and doing quite well: both have merged thousand PRs in the past 3 years to become more stable, they keep introducing new releases (see [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize)), and for now holds more features than Prisma. Also, both show solid performance (for an ORM). Hats off to the maintainers!\\n\\nBased on these observations, which should you pick? which ORM will we use for [practica.js](https://github.com/practicajs/practica)?\\n \\nPrisma is an excellent addition to Node.js ORMs family, but not the hassle-free one tool to rule them all. It\'s a mixed bag of many delicious candies and a few gotchas. Wouldn\'t it grow to tick all the boxes? Maybe, but unlikely. Once built, it\'s too hard to dramatically change the syntax and engine performance. Then, during the writing and speaking with the community, including some Prisma enthusiasts, I realized that it doesn\'t aim to be the can-do-everything \'Ferrari\'. Its positioning seems to resemble more a convenient family car with a solid engine and awesome user experience. In other words, it probably aims for the enterprise space where there is mostly demand for great DX, OK performance, and business-class support\\n\\nIn the end of this journey I see no dominant flawless \'Ferrari\' ORM. I should probably change my perspective: Building ORM for the hectic modern JavaScript ecosystem is 10x harder than building a Java ORM back then in 2001. There is no stain in the shirt, it\'s a cool JavaScript swag. I learned to accept what we have, a rich set of features, tolerable performance, good enough for many systems. Need more? Don\'t use ORM. Nothing is going to change dramatically, it\'s now as good as it can be\\n\\n### When will it shine?\\n\\n**Surely use Prisma under these scenarios -** If your data needs are rather simple; when time-to-market concern takes precedence over the data processing accuracy; when the DB is relatively small; if you\'re a mobile/frontend developer who is doing her first steps in the backend world; when there is a need for business-class support; AND when Prisma\'s long term business continuity risk is a non-issue for you\\n\\n**I\'d probably prefer other options under these conditions -** If the DB layer performance is a major concern; if you\'re savvy backend developer with solid SQL capabilities; when there is a need for fine grain control over the data layer. For all of these cases, Prisma might still work, but my primary choices would be using knex/TypeORM/Sequelize with a data-mapper style\\n\\nConsequently, we love Prisma and add it behind flag (--orm=prisma) to Practica.js. At the same time, until some clouds will disappear, Sequelize will remain our default ORM\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Popular Node.js patterns and tools to re-consider](https://practica.dev/blog/popular-nodejs-pattern-and-tools-to-reconsider)\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"popular-nodejs-pattern-and-tools-to-reconsider","metadata":{"permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/pattern-to-reconsider/index.md","source":"@site/blog/pattern-to-reconsider/index.md","title":"Popular Node.js patterns and tools to re-consider","description":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?","date":"2022-08-02T10:00:00.000Z","formattedDate":"August 2, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":21.09,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"popular-nodejs-pattern-and-tools-to-reconsider","date":"2022-08-02T10:00","hide_table_of_contents":true,"title":"Popular Node.js patterns and tools to re-consider","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"},"nextItem":{"title":"Practica.js v0.0.1 is alive","permalink":"/blog/practica-is-alive"}},"content":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?\\n\\nIn his novel book \'Atomic Habits\' the author James Clear states that:\\n\\n> \\"Mastery is created by habits. However, sometimes when we\'re on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot\\". In other words, practice makes perfect, and bad practices make things worst\\n\\nWe copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change\\n\\nLuckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples. \\n\\nAre those disruptive thoughts surely correct? I\'m not sure. There is one things I\'m sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not \\"don\'t use this tool!\\" but rather becoming familiar with other techniques that, _under some circumstances_ might be a better fit\\n\\n![Animals and frameworks shed their skin](./crab.webp)\\n\\n_The True Crab\'s exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell_\\n\\n\\n## TOC - Patterns to reconsider\\n\\n1. Dotenv\\n2. Calling a service from a controller\\n3. Nest.js dependency injection for all classes\\n4. Passport.js\\n5. Supertest\\n6. Fastify utility decoration\\n7. Logging from a catch clause\\n8. Morgan logger\\n9. NODE_ENV\\n\\n\x3c!--truncate--\x3e\\n## 1. Dotenv as your configuration source\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** A super popular technique in which the app configurable values (e.g., DB user name) are stored in a simple text file. Then, when the app loads, the dotenv library sets all the text file values as environment variables so the code can read this\\n\\n```javascript\\n// .env file\\nUSER_SERVICE_URL=https://users.myorg.com\\n\\n//start.js\\nrequire(\'dotenv\').config();\\n\\n//blog-post-service.js\\nrepository.savePost(post);\\n//update the user number of posts, read the users service URL from an environment variable\\nawait axios.put(`${process.env.USER_SERVICE_URL}/api/user/${post.userId}/incrementPosts`)\\n\\n```\\n\\n**\ud83d\udcca How popular:** 21,806,137 downloads/week!\\n\\n**\ud83e\udd14 Why it might be wrong:** Dotenv is so easy and intuitive to start with, so one might easily overlook fundamental features: For example, it\'s hard to infer the configuration schema and realize the meaning of each key and its typing. Consequently, there is no built-in way to fail fast when a mandatory key is missing - a flow might fail after starting and presenting some side effects (e.g., DB records were already mutated before the failure). In the example above, the blog post will be saved to DB, and only then will the code realize that a mandatory key is missing - This leaves the app hanging in an invalid state. On top of this, in the presence of many keys, it\'s impossible to organize them hierarchically. If not enough, it encourages developers to commit this .env file which might contain production values - this happens because there is no clear way to define development defaults. Teams usually work around this by committing .env.example file and then asking whoever pulls code to rename this file manually. If they remember to of course\\n\\n**\u2600\ufe0f Better alternative:** Some configuration libraries provide out of the box solution to all of these needs. They encourage a clear schema and the possibility to validate early and fail if needed. See [comparison of options here](https://practica.dev/decisions/configuration-library). One of the better alternatives is [\'convict\'](https://github.com/mozilla/node-convict), down below is the same example, this time with Convict, hopefully it\'s better now:\\n\\n```javascript\\n// config.js\\nexport default {\\n userService: {\\n url: {\\n // Hierarchical, documented and strongly typed \ud83d\udc47\\n doc: \\"The URL of the user management service including a trailing slash\\",\\n format: \\"url\\",\\n default: \\"http://localhost:4001\\",\\n nullable: false,\\n env: \\"USER_SERVICE_URL\\",\\n },\\n },\\n //more keys here\\n};\\n\\n//start.js\\nimport convict from \\"convict\\";\\nimport configSchema from \\"config\\";\\nconvict(configSchema);\\n// Fail fast!\\nconvictConfigurationProvider.validate();\\n\\n//blog-post.js\\nrepository.savePost(post);\\n// Will never arrive here if the URL is not set\\nawait axios.put(\\n `${convict.get(userService.url)}/api/user/${post.userId}/incrementPosts`\\n);\\n```\\n\\n## 2. Calling a \'fat\' service from the API controller\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Consider a reader of our code who wishes to understand the entire _high-level_ flow or delve into a very _specific_ part. She first lands on the API controller, where requests start. Unlike what its name implies, this controller layer is just an adapter and kept really thin and straightforward. Great thus far. Then the controller calls a big \'service\' with thousands of lines of code that represent the entire logic\\n\\n```javascript\\n// user-controller\\nrouter.post(\'/\', async (req, res, next) => {\\n await userService.add(req.body);\\n // Might have here try-catch or error response logic\\n}\\n\\n// user-service\\nexports function add(newUser){\\n // Want to understand quickly? Need to understand the entire user service, 1500 loc\\n // It uses technical language and reuse narratives of other flows\\n this.copyMoreFieldsToUser(newUser)\\n const doesExist = this.updateIfAlreadyExists(newUser)\\n if(!doesExist){\\n addToCache(newUser);\\n }\\n // 20 more lines that demand navigating to other functions in order to get the intent\\n}\\n\\n\\n```\\n\\n**\ud83d\udcca How popular:** It\'s hard to pull solid numbers here, I could confidently say that in _most_ of the app that I see, this is the case\\n\\n**\ud83e\udd14 Why it might be wrong:** We\'re here to tame complexities. One of the useful techniques is deferring a complexity to the later stage possible. In this case though, the reader of the code (hopefully) starts her journey through the tests and the controller - things are simple in these areas. Then, as she lands on the big service - she gets tons of complexity and small details, although she is focused on understanding the overall flow or some specific logic. This is **unnecessary** complexity\\n\\n**\u2600\ufe0f Better alternative:** The controller should call a particular type of service, a **use-case** , which is responsible for _summarizing_ the flow in a business and simple language. Each flow/feature is described using a use-case, each contains 4-10 lines of code, that tell the story without technical details. It mostly orchestrates other small services, clients, and repositories that hold all the implementation details. With use cases, the reader can grasp the high-level flow easily. She can now **choose** where she would like to focus. She is now exposed only to **necessary** complexity. This technique also encourages partitioning the code to the smaller object that the use-case orchestrates. Bonus: By looking at coverage reports, one can tell which features are covered, not just files/functions\\n\\nThis idea by the way is formalized in the [\'clean architecture\' book](https://www.bookdepository.com/Clean-Architecture-Robert-Martin/9780134494166?redirected=true&utm_medium=Google&utm_campaign=Base1&utm_source=IL&utm_content=Clean-Architecture&selectCurrency=ILS&w=AFF9AU99ZB4MTDA8VTRQ&gclid=Cj0KCQjw3eeXBhD7ARIsAHjssr92kqLn60dnfQCLjbkaqttdgvhRV5dqKtnY680GCNDvKp-16HtZp24aAg6GEALw_wcB) - I\'m not a big fan of \'fancy\' architectures, but see - it\'s worth cherry-picking techniques from every source. You may walk-through our [Node.js best practices starter, practica.js](https://github.com/practicajs/practica), and examine the use-cases code\\n\\n```javascript\\n// add-order-use-case.js\\nexport async function addOrder(newOrder: addOrderDTO) {\\n orderValidation.assertOrderIsValid(newOrder);\\n const userWhoOrdered = await userServiceClient.getUserWhoOrdered(\\n newOrder.userId\\n );\\n paymentTermsService.assertPaymentTerms(\\n newOrder.paymentTermsInDays,\\n userWhoOrdered.terms\\n );\\n\\n const response = await orderRepository.addOrder(newOrder);\\n\\n return response;\\n}\\n```\\n\\n## 3. Nest.js: Wire _everything_ with dependency injection\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** If you\'re doing Nest.js, besides having a powerful framework in your hands, you probably use DI for _everything_ and make every class injectable. Say you have a weather-service that depends upon humidity-service, and **there is no requirement to swap** the humidity-service with alternative providers. Nevertheless, you inject humidity-service into the weather-service. It becomes part of your development style, \\"why not\\" you think - I may need to stub it during testing or replace it in the future\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\n@Injectable()\\nexport class GoogleHumidityService {\\n\\n async getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n }\\n}\\n\\n// weather-service.ts - customer facing\\nimport { GoogleHumidityService } from \'./humidity-service.ts\';\\n\\nexport type weatherInfo{\\n temperature: number,\\n humidity: number\\n}\\n\\nexport class WeatherService {\\n constructor(private humidityService: GoogleHumidityService) {}\\n\\n async GetWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n }\\n}\\n\\n// app.module.ts\\n@Module({\\n providers: [GoogleHumidityService, WeatherService],\\n})\\nexport class AppModule {}\\n```\\n\\n**\ud83d\udcca How popular:** No numbers here but I could confidently say that in _all_ of the Nest.js app that I\'ve seen, this is the case. In the popular [\'nestjs-realworld-example-ap[p\'](](https://github.com/lujakob/nestjs-realworld-example-app)) all the services are \'injectable\'\\n\\n**\ud83e\udd14 Why it might be wrong:** Dependency injection is not a priceless coding style but a pattern you should pull in the right moment, like any other pattern. Why? Because any pattern has a price. What price, you ask? First, encapsulation is violated. Clients of the weather-service are now aware that other providers are being used _internally_. Some clients may get tempted to override providers also it\'s not under their responsibility. Second, it\'s another layer of complexity to learn, maintain, and one more way to shoot yourself in the legs. StackOverflow owes some of its revenues to Nest.js DI - plenty of discussions try to solve this puzzle (e.g. did you know that in case of circular dependencies the order of imports matters?). Third, there is the performance thing - Nest.js, for example struggled to provide a decent start time for serverless environments and had to introduce [lazy loaded modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules). Don\'t get me wrong, **in some cases**, there is a good case for DI: When a need arises to decouple a dependency from its caller, or to allow clients to inject custom implementations (e.g., the strategy pattern). **In such case**, when there is a value, you may consider whether the _value of DI is worth its price_. If you don\'t have this case, why pay for nothing?\\n\\nI recommend reading the first paragraphs of this blog post [\'Dependency Injection is EVIL\'](https://www.tonymarston.net/php-mysql/dependency-injection-is-evil.html) (and absolutely don\'t agree with this bold words)\\n\\n**\u2600\ufe0f Better alternative:** \'Lean-ify\' your engineering approach - avoid using any tool unless it serves a real-world need immediately. Start simple, a dependent class should simply import its dependency and use it - Yeah, using the plain Node.js module system (\'require\'). Facing a situation when there is a need to factor dynamic objects? There are a handful of simple patterns, simpler than DI, that you should consider, like \'if/else\', factory function, and more. Are singletons requested? Consider techniques with lower costs like the module system with factory function. Need to stub/mock for testing? Monkey patching might be better than DI: better clutter your test code a bit than clutter your production code. Have a strong need to hide from an object where its dependencies are coming from? You sure? Use DI!\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\nexport async function getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n}\\n\\n// weather-service.ts - customer facing\\nimport { getHumidity } from \\"./humidity-service.ts\\";\\n\\n// \u2705 No wiring is happening externally, all is flat and explicit. Simple\\nexport async function getWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n // Nobody needs to know about it, its an implementation details\\n await getHumidity(when);\\n}\\n```\\n\\n___\\n\\n## 1 min pause: A word or two about me, the author\\n\\nMy name is Yoni Goldberg, I\'m a Node.js developer and consultant. I wrote few code-books like [JavaScript testing best practices](https://github.com/goldbergyoni/javascript-testing-best-practices) and [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices) (100,000 stars \u2728\ud83e\udd79). That said, my best guide is [Node.js testing practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices) which only few read \ud83d\ude1e. I shall release [an advanced Node.js testing course soon](https://testjavascript.com/) and also hold workshops for teams. I\'m also a core maintainer of [Practica.js](https://github.com/practicajs/practica) which is a Node.js starter that creates a production-ready example Node Monorepo solution that is based on the standards and simplicity. It might be your primary option when starting a new Node.js solution\\n\\n___\\n\\n## 4. Passport.js for token authentication\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Commonly, you\'re in need to issue or/and authenticate JWT tokens. Similarly, you might need to allow login from _one_ single social network like Google/Facebook. When faced with these kinds of needs, Node.js developers rush to the glorious library [Passport.js](https://www.passportjs.org/) like butterflies are attracted to light\\n\\n**\ud83d\udcca How popular:** 1,389,720 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** When tasked with guarding your routes with JWT token - you\'re just a few lines of code shy from ticking the goal. Instead of messing up with a new framework, instead of introducing levels of indirections (you call passport, then it calls you), instead of spending time learning new abstractions - use a JWT library directly. Libraries like [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) or [fast-jwt](https://github.com/nearform/fast-jwt) are simple and well maintained. Have concerns with the security hardening? Good point, your concerns are valid. But would you not get better hardening with a direct understanding of your configuration and flow? Will hiding things behind a framework help? Even if you prefer the hardening of a battle-tested framework, Passport doesn\'t handle a handful of security risks like secrets/token, secured user management, DB protection, and more. My point, you probably anyway need fully-featured user and authentication management platforms. Various cloud services and OSS projects, can tick all of those security concerns. Why then start in the first place with a framework that doesn\'t satisfy your security needs? It seems like many who opt for Passport.js are not fully aware of which needs are satisfied and which are left open. All of that said, Passport definitely shines when looking for a quick way to support _many_ social login providers\\n\\n**\u2600\ufe0f Better alternative:** Is token authentication in order? These few lines of code below might be all you need. You may also glimpse into [Practica.js wrapper around these libraries](https://github.com/practicajs/practica/tree/main/src/code-templates/libraries/jwt-token-verifier). A real-world project at scale typically need more: supporting async JWT [(JWKS)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets), securely manage and rotate the secrets to name a few examples. In this case, OSS solution like [keycloak (https://github.com/keycloak/keycloak) or commercial options like Auth0[https://github.com/auth0] are alternatives to consider\\n\\n```javascript\\n// jwt-middleware.js, a simplified version - Refer to Practica.js to see some more corner cases\\nconst middleware = (req, res, next) => {\\n if(!req.headers.authorization){\\n res.sendStatus(401)\\n }\\n\\n jwt.verify(req.headers.authorization, options.secret, (err: any, jwtContent: any) => {\\n if (err) {\\n return res.sendStatus(401);\\n }\\n\\n req.user = jwtContent.data;\\n\\n next();\\n });\\n```\\n\\n## 5. Supertest for integration/API testing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** When testing against an API (i.e., component, integration, E2E tests), the library [supertest](https://www.npmjs.com/package/supertest) provides a sweet syntax that can both detect the web server address, make HTTP call and also assert on the response. Three in one\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400\\", (done) => {\\n const request = require(\\"supertest\\");\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n request(app)\\n .post(\\"/user\\")\\n .send(userToAdd)\\n .expect(\\"Content-Type\\", /json/)\\n .expect(400, done);\\n\\n // Assert\\n // We already asserted above \u261d\ud83c\udffb as part of the request\\n});\\n```\\n\\n**\ud83d\udcca How popular:** 2,717,744 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** You already have your assertion library (Jest? Chai?), it has a great error highlighting and comparison - you trust it. Why code some tests using another assertion syntax? Not to mention, Supertest\'s assertion errors are not as descriptive as Jest and Chai. It\'s also cumbersome to mix HTTP client + assertion library instead of choosing the best for each mission. Speaking of the best, there are more standard, popular, and better-maintained HTTP clients (like fetch, axios and other friends). Need another reason? Supertest might encourage coupling the tests to Express as it offers a constructor that gets an Express object. This constructor infers the API address automatically (useful when using dynamic test ports). This couples the test to the implementation and won\'t work in the case where you wish to run the same tests against a remote process (the API doesn\'t live with the tests). My repository [\'Node.js testing best practices\'](https://github.com/testjavascript/nodejs-integration-tests-best-practices) holds examples of how tests can infer the API port and address\\n\\n**\u2600\ufe0f Better alternative:** A popular and standard HTTP client library like Node.js Fetch or Axios. In [Practica.js](https://github.com/practicajs/practica) (a Node.js starter that packs many best practices) we use Axios. It allows us to configure a HTTP client that is shared among all the tests: We bake inside a JWT token, headers, and a base URL. Another good pattern that we look at, is making each Microservice generate HTTP client library for its consumers. This brings strong-type experience to the clients, synchronizes the provider-consumer versions and as a bonus - The provider can test itself with the same library that its consumers are using\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400 and includes a reason\\", (done) => {\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n const receivedResponse = axios.post(\\n `http://localhost:${apiPort}/user`,\\n userToAdd\\n );\\n\\n // Assert\\n // \u2705 Assertion happens in a dedicated stage and a dedicated library\\n expect(receivedResponse).toMatchObject({\\n status: 400,\\n data: {\\n reason: \\"no-name\\",\\n },\\n });\\n});\\n```\\n\\n## 6. Fastify decorate for non request/web utilities\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** [Fastify](https://github.com/fastify/fastify) introduces great patterns. Personally, I highly appreciate how it preserves the simplicity of Express while bringing more batteries. One thing that got me wondering is the \'decorate\' feature which allows placing common utilities/services inside a widely accessible container object. I\'m referring here specifically to the case where a cross-cutting concern utility/service is being used. Here is an example:\\n\\n```javascript\\n// An example of a utility that is cross-cutting-concern. Could be logger or anything else\\nfastify.decorate(\'metricsService\', function (name) {\\n fireMetric: () => {\\n // My code that sends metrics to the monitoring system\\n }\\n})\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n this.metricsService.fireMetric({name: \'new-request\'})\\n // Handle the request\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n // How to fire a metric?\\n}\\n```\\n\\nIt should be noted that \'decoration\' is also used to place values (e.g., user) inside a request - this is a slightly different case and a sensible one\\n\\n**\ud83d\udcca How popular:** Fastify has 696,122 weekly download and growing rapidly. The decorator concept is part of the framework\'s core\\n\\n**\ud83e\udd14 Why it might be wrong:** Some services and utilities serve cross-cutting-concern needs and should be accessible from other layers like domain (i.e, business logic, DAL). When placing utilities inside this object, the Fastify object might not be accessible to these layers. You probably don\'t want to couple your web framework with your business logic: Consider that some of your business logic and repositories might get invoked from non-REST clients like CRON, MQ, and similar - In these cases, Fastify won\'t get involved at all so better not trust it to be your service locator\\n\\n**\u2600\ufe0f Better alternative:** A good old Node.js module is a standard way to expose and consume functionality. Need a singleton? Use the module system caching. Need to instantiate a service in correlation with a Fastify life-cycle hook (e.g., DB connection on start)? Call it from that Fastify hook. In the rare case where a highly dynamic and complex instantiation of dependencies is needed - DI is also a (complex) option to consider\\n\\n```javascript\\n// \u2705 A simple usage of good old Node.js modules\\n// metrics-service.js\\n\\nexports async function fireMetric(name){\\n // My code that sends metrics to the monitoring system\\n}\\n\\nimport {fireMetric} from \'./metrics-service.js\'\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n metricsService.fireMetric({name: \'new-request\'})\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n metricsService.fireMetric({name: \'new-request\'})\\n}\\n```\\n\\n## 7. Logging from a catch clause\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** You catch an error somewhere deep in the code (not on the route level), then call logger.error to make this error observable. Seems simple and necessary\\n\\n```javascript\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n logger.error(error, this, {operation: addNewOrder});\\n}\\n```\\n\\n**\ud83d\udcca How popular:** Hard to put my hands on numbers but it\'s quite popular, right?\\n\\n**\ud83e\udd14 Why it might be wrong:** First, errors should get handled/logged in a central location. Error handling is a critical path. Various catch clauses are likely to behave differently without a centralized and unified behavior. For example, a request might arise to tag all errors with certain metadata, or on top of logging, to also fire a monitoring metric. Applying these requirements in ~100 locations is not a walk in the park. Second, catch clauses should be minimized to particular scenarios. By default, the natural flow of an error is bubbling down to the route/entry-point - from there, it will get forwarded to the error handler. Catch clauses are more verbose and error-prone - therefore it should serve two very specific needs: When one wishes to change the flow based on the error or enrich the error with more information (which is not the case in this example)\\n\\n**\u2600\ufe0f Better alternative:** By default, let the error bubble down the layers and get caught by the entry-point global catch (e.g., Express error middleware). In cases when the error should trigger a different flow (e.g., retry) or there is value in enriching the error with more context - use a catch clause. In this case, ensure the .catch code also reports to the error handler\\n\\n```javascript\\n// A case where we wish to retry upon failure\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n // \u2705 A central location that handles error\\n errorHandler.handle(error, this, {operation: addNewOrder});\\n callTheUserService(numOfRetries++);\\n}\\n```\\n\\n## 8. Use Morgan logger for express web requests\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** In many web apps, you are likely to find a pattern that is being copy-pasted for ages - Using Morgan logger to log requests information:\\n\\n```javascript\\nconst express = require(\\"express\\");\\nconst morgan = require(\\"morgan\\");\\n\\nconst app = express();\\n\\napp.use(morgan(\\"combined\\"));\\n```\\n\\n**\ud83d\udcca How popular:** 2,901,574 downloads/week\\n\\n**\ud83e\udd14 Why it might be wrong:** Wait a second, you already have your main logger, right? Is it Pino? Winston? Something else? Great. Why deal with and configure yet another logger? I do appreciate the HTTP domain-specific language (DSL) of Morgan. The syntax is sweet! But does it justify having two loggers?\\n\\n**\u2600\ufe0f Better alternative:** Put your chosen logger in a middleware and log the desired request/response properties:\\n\\n```javascript\\n// \u2705 Use your preferred logger for all the tasks\\nconst logger = require(\\"pino\\")();\\napp.use((req, res, next) => {\\n res.on(\\"finish\\", () => {\\n logger.info(`${req.url} ${res.statusCode}`); // Add other properties here\\n });\\n next();\\n});\\n```\\n\\n## 9. Having conditional code based on `NODE_ENV` value\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** To differentiate between development vs production configuration, it\'s common to set the environment variable NODE_ENV with \\"production|test\\". Doing so allows the various tooling to act differently. For example, some templating engines will cache compiled templates only in production. Beyond tooling, custom applications use this to specify behaviours that are unique to the development or production environment:\\n\\n```javascript\\nif (process.env.NODE_ENV === \\"production\\") {\\n // This is unlikely to be tested since test runner usually set NODE_ENV=test\\n setLogger({ stdout: true, prettyPrint: false });\\n // If this code branch above exists, why not add more production-only configurations:\\n collectMetrics();\\n} else {\\n setLogger({ splunk: true, prettyPrint: true });\\n}\\n```\\n\\n**\ud83d\udcca How popular:** 5,034,323 code results in GitHub when searching for \\"NODE_ENV\\". It doesn\'t seem like a rare pattern\\n\\n**\ud83e\udd14 Why it might be wrong:** Anytime your code checks whether it\'s production or not, this branch won\'t get hit by default in some test runner (e.g., Jest set `NODE_ENV=test`). In _any_ test runner, the developer must remember to test for each possible value of this environment variable. In the example above, `collectMetrics()` will be tested for the first time in production. Sad smiley. Additionally, putting these conditions opens the door to add more differences between production and the developer machine - when this variable and conditions exists, a developer gets tempted to put some logic for production only. Theoretically, this can be tested: one can set `NODE_ENV = \\"production\\"` in testing and cover the production branches (if she remembers...). But then, if you can test with `NODE_ENV=\'production\'`, what\'s the point in separating? Just consider everything to be \'production\' and avoid this error-prone mental load\\n\\n**\u2600\ufe0f Better alternative:** Any code that was written by us, must be tested. This implies avoiding any form of if(production)/else(development) conditions. Wouldn\'t anyway developers machine have different surrounding infrastructure than production (e.g., logging system)? They do, the environments are quite difference, but we feel comfortable with it. These infrastructural things are battle-tested, extraneous, and not part of our code. To keep the same code between dev/prod and still use different infrastructure - we put different values in the configuration (not in the code). For example, a typical logger emits JSON in production but in a development machine it emits \'pretty-print\' colorful lines. To meet this, we set ENV VAR that tells whether what logging style we aim for:\\n\\n```javascript\\n//package.json\\n\\"scripts\\": {\\n \\"start\\": \\"LOG_PRETTY_PRINT=false index.js\\",\\n \\"test\\": \\"LOG_PRETTY_PRINT=true jest\\"\\n}\\n\\n//index.js\\n//\u2705 No condition, same code for all the environments. The variations are defined externally in config or deployment files\\nsetLogger({prettyPrint: process.env.LOG_PRETTY_PRINT})\\n```\\n\\n## Closing\\n\\nI hope that these thoughts, at least one of them, made you re-consider adding a new technique to your toolbox. In any case, let\'s keep our community vibrant, disruptive and kind. Respectful discussions are almost as important as the event loop. Almost.\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [How to be a better Node.js developer in 2020](https://yonigoldberg.medium.com/20-ways-to-become-a-better-node-js-developer-in-2020-d6bd73fcf424). The 2023 version is coming soon\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"practica-is-alive","metadata":{"permalink":"/blog/practica-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/practica-is-alive/index.md","source":"@site/blog/practica-is-alive/index.md","title":"Practica.js v0.0.1 is alive","description":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.","date":"2022-07-15T10:00:00.000Z","formattedDate":"July 15, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"fastify","permalink":"/blog/tags/fastify"}],"readingTime":1.21,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"practica-is-alive","date":"2022-07-15T10:00","hide_table_of_contents":true,"title":"Practica.js v0.0.1 is alive","authors":["goldbergyoni"],"tags":["node.js","express","fastify"]},"prevItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.\\n\\n## What is Practica is one paragraph\\n\\nAlthough Node.js has great frameworks \ud83d\udc9a, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are [neatly and thoughtfully documented](./decisions/index). We strive to keep things as simple and standard as possible and base our work off the popular guide: [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices).\\n\\nYour developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you\'ll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app. \\n\\n## 90 seconds video\\n\\n\\n\\n## How to get started\\n\\nTo get up to speed quickly, read our [getting started guide](https://practica.dev/the-basics/getting-started-quickly)."}]}')}}]); \ No newline at end of file diff --git a/assets/js/d2a399e8.f2cd1949.js b/assets/js/d2a399e8.bd08fe86.js similarity index 99% rename from assets/js/d2a399e8.f2cd1949.js rename to assets/js/d2a399e8.bd08fe86.js index a2c32fa6..f4735d97 100644 --- a/assets/js/d2a399e8.f2cd1949.js +++ b/assets/js/d2a399e8.bd08fe86.js @@ -1 +1 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[366],{3905:(e,o,t)=>{t.d(o,{Zo:()=>p,kt:()=>m});var r=t(7294);function n(e,o,t){return o in e?Object.defineProperty(e,o,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[o]=t,e}function a(e,o){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);o&&(r=r.filter((function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var o=1;o=0||(n[t]=e[t]);return n}(e,o);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(n[t]=e[t])}return n}var l=r.createContext({}),c=function(e){var o=r.useContext(l),t=o;return e&&(t="function"==typeof e?e(o):i(i({},o),e)),t},p=function(e){var o=c(e.components);return r.createElement(l.Provider,{value:o},e.children)},h={inlineCode:"code",wrapper:function(e){var o=e.children;return r.createElement(r.Fragment,{},o)}},u=r.forwardRef((function(e,o){var t=e.components,n=e.mdxType,a=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),u=c(t),m=n,d=u["".concat(l,".").concat(m)]||u[m]||h[m]||a;return t?r.createElement(d,i(i({ref:o},p),{},{components:t})):r.createElement(d,i({ref:o},p))}));function m(e,o){var t=arguments,n=o&&o.mdxType;if("string"==typeof e||n){var a=t.length,i=new Array(a);i[0]=u;var s={};for(var l in o)hasOwnProperty.call(o,l)&&(s[l]=o[l]);s.originalType=e,s.mdxType="string"==typeof e?e:n,i[1]=s;for(var c=2;c{t.r(o),t.d(o,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var r=t(7462),n=(t(7294),t(3905));const a={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},i="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-07T05:31:27.000Z",formattedDate:"July 7, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],p={toc:c};function h(e){let{components:o,...a}=e;return(0,n.kt)("wrapper",(0,r.Z)({},p,a,{components:o,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"Monorepos",src:t(1377).Z,width:"1400",height:"796"})),(0,n.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,n.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,n.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,n.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,n.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,n.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"))}h.isMDXComponent=!0},1377:(e,o,t)=>{t.d(o,{Z:()=>r});const r=t.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[366],{3905:(e,o,t)=>{t.d(o,{Zo:()=>p,kt:()=>m});var r=t(7294);function n(e,o,t){return o in e?Object.defineProperty(e,o,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[o]=t,e}function a(e,o){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);o&&(r=r.filter((function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var o=1;o=0||(n[t]=e[t]);return n}(e,o);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(n[t]=e[t])}return n}var l=r.createContext({}),c=function(e){var o=r.useContext(l),t=o;return e&&(t="function"==typeof e?e(o):i(i({},o),e)),t},p=function(e){var o=c(e.components);return r.createElement(l.Provider,{value:o},e.children)},h={inlineCode:"code",wrapper:function(e){var o=e.children;return r.createElement(r.Fragment,{},o)}},u=r.forwardRef((function(e,o){var t=e.components,n=e.mdxType,a=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),u=c(t),m=n,d=u["".concat(l,".").concat(m)]||u[m]||h[m]||a;return t?r.createElement(d,i(i({ref:o},p),{},{components:t})):r.createElement(d,i({ref:o},p))}));function m(e,o){var t=arguments,n=o&&o.mdxType;if("string"==typeof e||n){var a=t.length,i=new Array(a);i[0]=u;var s={};for(var l in o)hasOwnProperty.call(o,l)&&(s[l]=o[l]);s.originalType=e,s.mdxType="string"==typeof e?e:n,i[1]=s;for(var c=2;c{t.r(o),t.d(o,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var r=t(7462),n=(t(7294),t(3905));const a={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},i="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-07T05:38:19.000Z",formattedDate:"July 7, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],p={toc:c};function h(e){let{components:o,...a}=e;return(0,n.kt)("wrapper",(0,r.Z)({},p,a,{components:o,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"Monorepos",src:t(1377).Z,width:"1400",height:"796"})),(0,n.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,n.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,n.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,n.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,n.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,n.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"))}h.isMDXComponent=!0},1377:(e,o,t)=>{t.d(o,{Z:()=>r});const r=t.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file diff --git a/assets/js/e25a597f.58f4e179.js b/assets/js/e25a597f.b2cc51c1.js similarity index 99% rename from assets/js/e25a597f.58f4e179.js rename to assets/js/e25a597f.b2cc51c1.js index e560bba2..0cc71242 100644 --- a/assets/js/e25a597f.58f4e179.js +++ b/assets/js/e25a597f.b2cc51c1.js @@ -1 +1 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9768],{3905:(e,t,o)=>{o.d(t,{Zo:()=>c,kt:()=>u});var n=o(7294);function a(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function i(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,n)}return o}function r(e){for(var t=1;t=0||(a[o]=e[o]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,o)&&(a[o]=e[o])}return a}var l=n.createContext({}),h=function(e){var t=n.useContext(l),o=t;return e&&(o="function"==typeof e?e(t):r(r({},t),e)),o},c=function(e){var t=h(e.components);return n.createElement(l.Provider,{value:t},e.children)},p={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var o=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=h(o),u=a,m=d["".concat(l,".").concat(u)]||d[u]||p[u]||i;return o?n.createElement(m,r(r({ref:t},c),{},{components:o})):n.createElement(m,r({ref:t},c))}));function u(e,t){var o=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=o.length,r=new Array(i);r[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s.mdxType="string"==typeof e?e:a,r[1]=s;for(var h=2;h{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>p,frontMatter:()=>i,metadata:()=>s,toc:()=>h});var n=o(7462),a=(o(7294),o(3905));const i={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},r="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-07T05:31:27.000Z",formattedDate:"July 7, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},h=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],c={toc:h};function p(e){let{components:t,...i}=e;return(0,a.kt)("wrapper",(0,n.Z)({},c,i,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Monorepos",src:o(1377).Z,width:"1400",height:"796"})),(0,a.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,a.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,a.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,a.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,a.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,a.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"),(0,a.kt)("h1",{id:"layer-1-plain-old-folders-to-stay-on-top-of-your-code"},"Layer 1: Plain old folders to stay on top of your code"),(0,a.kt)("p",null,"With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, ",(0,a.kt)("em",{parentName:"p"},"quickly")," adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity."),(0,a.kt)("p",null,"This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling."),(0,a.kt)("p",null,"With that said, some of the newer tools augment this experience with interesting features:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Both ",(0,a.kt)("a",{parentName:"li",href:"https://turborepo.org/"},"Turborepo")," and ",(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/dependency-graph"},"Nx")," and also ",(0,a.kt)("a",{parentName:"li",href:"https://www.npmjs.com/package/lerna-dependency-graph"},"Lerna")," provide a visual representation of the packages\u2019 dependencies"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/monorepo-tags"},"Nx allows \u2018visibility rules\u2019")," which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg",alt:null})),(0,a.kt)("p",null,"Nx dependencies graph"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/generators/workspace-generators"},"Nx workspace generator")," allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing")),(0,a.kt)("h1",{id:"layer-2-tasks-and-pipeline-to-build-your-code-efficiently"},"Layer 2: Tasks and pipeline to build your code efficiently"),(0,a.kt)("p",null,"Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of ",(0,a.kt)("em",{parentName:"p"},"multiple")," components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png",alt:null})),(0,a.kt)("p",null,"Apply some commands over multiple packages"),(0,a.kt)("p",null,"In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 ",(0,a.kt)("em",{parentName:"p"},"deeply")," optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Parallelization \u2014")," If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Smart execution plan \u2014"),"Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s ",(0,a.kt)("em",{parentName:"li"},"isolated")," unit tests ",(0,a.kt)("em",{parentName:"li"},"while")," building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png",alt:null})),(0,a.kt)("p",null,"A modern tool advantage over old Lerna. Taken from Turborepo website"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Detect who is affected by a change \u2014")," Even on a system with high coupling between packages, it\u2019s usually not necessary to run ",(0,a.kt)("em",{parentName:"li"},"all")," packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Sub-systems (i.e., projects) \u2014")," Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Caching \u2014")," This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Remote caching \u2014")," Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time")),(0,a.kt)("h1",{id:"layer-3-hoist-your-dependencies-to-boost-npm-installation"},"Layer 3: Hoist your dependencies to boost npm installation"),(0,a.kt)("p",null,"The speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (",(0,a.kt)("a",{parentName:"p",href:"https://rushjs.io/pages/advanced/npm_doppelgangers/"},"the NPM doppelgangers problem"),") the installation of those packages which might result in a long process."),(0,a.kt)("p",null,"This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern."),(0,a.kt)("p",null,"Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png",alt:null})),(0,a.kt)("p",null,"On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default."),(0,a.kt)("p",null,"I\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js ",(0,a.kt)("em",{parentName:"p"},"runtime")," and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019."),(0,a.kt)("h1",{id:"stop-for-a-second-its-all-about-your-workflow"},"Stop for a second: It\u2019s all about your workflow"),(0,a.kt)("p",null,"We deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is ",(0,a.kt)("em",{parentName:"p"},"synchronized or independent")," (we will discuss this in a few seconds)",(0,a.kt)("em",{parentName:"p"},".")," This upfront ",(0,a.kt)("em",{parentName:"p"},"fundamental")," decision will change almost everything."),(0,a.kt)("p",null,"Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option A \u2014 The synchronized workflow-")," Going with this development style, all the three components will be developed and deployed in one chunk ",(0,a.kt)("em",{parentName:"p"},"together"),". Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed ",(0,a.kt)("em",{parentName:"p"},"together.")),(0,a.kt)("p",null,"Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option B \u2014 Independent workflow-")," This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it."),(0,a.kt)("p",null,"Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also ",(0,a.kt)("em",{parentName:"p"},"forces her")," to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png",alt:null})),(0,a.kt)("p",null,"Synchronized and independent workflows illustrated"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"On the illusion of synchronicity")),(0,a.kt)("p",null,"In distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear."),(0,a.kt)("p",null,"This fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph."),(0,a.kt)("h1",{id:"layer-4-link-your-packages-for-immediate-feedback"},"Layer 4: Link your packages for immediate feedback"),(0,a.kt)("p",null,"When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option 1: Using npm \u2014")," Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option2: Just a plain folder \u2014")," With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented."),(0,a.kt)("p",null,"With option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes."),(0,a.kt)("p",null,"With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers."),(0,a.kt)("p",null,"How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png",alt:null})),(0,a.kt)("p",null,"Linking packages in a Monorepo"),(0,a.kt)("p",null,"If you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it."),(0,a.kt)("p",null,"If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is ",(0,a.kt)("em",{parentName:"p"},"temporarily link")," (with npm link) between the packages",(0,a.kt)("em",{parentName:"p"},",")," debug, code, then finally remove the link."),(0,a.kt)("p",null,"Nx is taking a slightly more disruptive approach \u2014 it is using ",(0,a.kt)("a",{parentName:"p",href:"https://www.typescriptlang.org/tsconfig#paths"},"TypeScript paths")," to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work."),(0,a.kt)("h1",{id:"closing-what-should-you-use"},"Closing: What should you use?"),(0,a.kt)("p",null,"It\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario A \u2014")," If your architecture dictates a ",(0,a.kt)("em",{parentName:"p"},"synchronized workflow")," where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice."),(0,a.kt)("p",null,"For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many ",(0,a.kt)("em",{parentName:"p"},"relatively")," coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked."),(0,a.kt)("p",null,"If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png",alt:null})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario B\u2014")," If you\u2019re into an ",(0,a.kt)("em",{parentName:"p"},"independent workflow")," where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/giltayar/bilt"},"Bilt")," by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"In any scenario, consider workspaces \u2014")," If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain."),(0,a.kt)("p",null,"We tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes."),(0,a.kt)("h1",{id:"bonus-comparison-table"},"Bonus: Comparison table"),(0,a.kt)("p",null,"See below a detailed comparison table of the various tools and features:"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png",alt:null})),(0,a.kt)("p",null,"Preview only, the complete table can be ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md"},"found here")))}p.isMDXComponent=!0},1377:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9768],{3905:(e,t,o)=>{o.d(t,{Zo:()=>c,kt:()=>u});var n=o(7294);function a(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function i(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,n)}return o}function r(e){for(var t=1;t=0||(a[o]=e[o]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,o)&&(a[o]=e[o])}return a}var l=n.createContext({}),h=function(e){var t=n.useContext(l),o=t;return e&&(o="function"==typeof e?e(t):r(r({},t),e)),o},c=function(e){var t=h(e.components);return n.createElement(l.Provider,{value:t},e.children)},p={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var o=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=h(o),u=a,m=d["".concat(l,".").concat(u)]||d[u]||p[u]||i;return o?n.createElement(m,r(r({ref:t},c),{},{components:o})):n.createElement(m,r({ref:t},c))}));function u(e,t){var o=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=o.length,r=new Array(i);r[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s.mdxType="string"==typeof e?e:a,r[1]=s;for(var h=2;h{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>p,frontMatter:()=>i,metadata:()=>s,toc:()=>h});var n=o(7462),a=(o(7294),o(3905));const i={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},r="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-07T05:38:19.000Z",formattedDate:"July 7, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},h=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],c={toc:h};function p(e){let{components:t,...i}=e;return(0,a.kt)("wrapper",(0,n.Z)({},c,i,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Monorepos",src:o(1377).Z,width:"1400",height:"796"})),(0,a.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,a.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,a.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,a.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,a.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,a.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"),(0,a.kt)("h1",{id:"layer-1-plain-old-folders-to-stay-on-top-of-your-code"},"Layer 1: Plain old folders to stay on top of your code"),(0,a.kt)("p",null,"With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, ",(0,a.kt)("em",{parentName:"p"},"quickly")," adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity."),(0,a.kt)("p",null,"This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling."),(0,a.kt)("p",null,"With that said, some of the newer tools augment this experience with interesting features:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Both ",(0,a.kt)("a",{parentName:"li",href:"https://turborepo.org/"},"Turborepo")," and ",(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/dependency-graph"},"Nx")," and also ",(0,a.kt)("a",{parentName:"li",href:"https://www.npmjs.com/package/lerna-dependency-graph"},"Lerna")," provide a visual representation of the packages\u2019 dependencies"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/monorepo-tags"},"Nx allows \u2018visibility rules\u2019")," which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg",alt:null})),(0,a.kt)("p",null,"Nx dependencies graph"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/generators/workspace-generators"},"Nx workspace generator")," allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing")),(0,a.kt)("h1",{id:"layer-2-tasks-and-pipeline-to-build-your-code-efficiently"},"Layer 2: Tasks and pipeline to build your code efficiently"),(0,a.kt)("p",null,"Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of ",(0,a.kt)("em",{parentName:"p"},"multiple")," components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png",alt:null})),(0,a.kt)("p",null,"Apply some commands over multiple packages"),(0,a.kt)("p",null,"In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 ",(0,a.kt)("em",{parentName:"p"},"deeply")," optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Parallelization \u2014")," If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Smart execution plan \u2014"),"Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s ",(0,a.kt)("em",{parentName:"li"},"isolated")," unit tests ",(0,a.kt)("em",{parentName:"li"},"while")," building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png",alt:null})),(0,a.kt)("p",null,"A modern tool advantage over old Lerna. Taken from Turborepo website"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Detect who is affected by a change \u2014")," Even on a system with high coupling between packages, it\u2019s usually not necessary to run ",(0,a.kt)("em",{parentName:"li"},"all")," packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Sub-systems (i.e., projects) \u2014")," Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Caching \u2014")," This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Remote caching \u2014")," Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time")),(0,a.kt)("h1",{id:"layer-3-hoist-your-dependencies-to-boost-npm-installation"},"Layer 3: Hoist your dependencies to boost npm installation"),(0,a.kt)("p",null,"The speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (",(0,a.kt)("a",{parentName:"p",href:"https://rushjs.io/pages/advanced/npm_doppelgangers/"},"the NPM doppelgangers problem"),") the installation of those packages which might result in a long process."),(0,a.kt)("p",null,"This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern."),(0,a.kt)("p",null,"Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png",alt:null})),(0,a.kt)("p",null,"On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default."),(0,a.kt)("p",null,"I\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js ",(0,a.kt)("em",{parentName:"p"},"runtime")," and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019."),(0,a.kt)("h1",{id:"stop-for-a-second-its-all-about-your-workflow"},"Stop for a second: It\u2019s all about your workflow"),(0,a.kt)("p",null,"We deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is ",(0,a.kt)("em",{parentName:"p"},"synchronized or independent")," (we will discuss this in a few seconds)",(0,a.kt)("em",{parentName:"p"},".")," This upfront ",(0,a.kt)("em",{parentName:"p"},"fundamental")," decision will change almost everything."),(0,a.kt)("p",null,"Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option A \u2014 The synchronized workflow-")," Going with this development style, all the three components will be developed and deployed in one chunk ",(0,a.kt)("em",{parentName:"p"},"together"),". Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed ",(0,a.kt)("em",{parentName:"p"},"together.")),(0,a.kt)("p",null,"Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option B \u2014 Independent workflow-")," This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it."),(0,a.kt)("p",null,"Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also ",(0,a.kt)("em",{parentName:"p"},"forces her")," to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png",alt:null})),(0,a.kt)("p",null,"Synchronized and independent workflows illustrated"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"On the illusion of synchronicity")),(0,a.kt)("p",null,"In distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear."),(0,a.kt)("p",null,"This fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph."),(0,a.kt)("h1",{id:"layer-4-link-your-packages-for-immediate-feedback"},"Layer 4: Link your packages for immediate feedback"),(0,a.kt)("p",null,"When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option 1: Using npm \u2014")," Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option2: Just a plain folder \u2014")," With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented."),(0,a.kt)("p",null,"With option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes."),(0,a.kt)("p",null,"With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers."),(0,a.kt)("p",null,"How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png",alt:null})),(0,a.kt)("p",null,"Linking packages in a Monorepo"),(0,a.kt)("p",null,"If you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it."),(0,a.kt)("p",null,"If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is ",(0,a.kt)("em",{parentName:"p"},"temporarily link")," (with npm link) between the packages",(0,a.kt)("em",{parentName:"p"},",")," debug, code, then finally remove the link."),(0,a.kt)("p",null,"Nx is taking a slightly more disruptive approach \u2014 it is using ",(0,a.kt)("a",{parentName:"p",href:"https://www.typescriptlang.org/tsconfig#paths"},"TypeScript paths")," to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work."),(0,a.kt)("h1",{id:"closing-what-should-you-use"},"Closing: What should you use?"),(0,a.kt)("p",null,"It\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario A \u2014")," If your architecture dictates a ",(0,a.kt)("em",{parentName:"p"},"synchronized workflow")," where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice."),(0,a.kt)("p",null,"For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many ",(0,a.kt)("em",{parentName:"p"},"relatively")," coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked."),(0,a.kt)("p",null,"If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png",alt:null})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario B\u2014")," If you\u2019re into an ",(0,a.kt)("em",{parentName:"p"},"independent workflow")," where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/giltayar/bilt"},"Bilt")," by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"In any scenario, consider workspaces \u2014")," If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain."),(0,a.kt)("p",null,"We tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes."),(0,a.kt)("h1",{id:"bonus-comparison-table"},"Bonus: Comparison table"),(0,a.kt)("p",null,"See below a detailed comparison table of the various tools and features:"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png",alt:null})),(0,a.kt)("p",null,"Preview only, the complete table can be ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md"},"found here")))}p.isMDXComponent=!0},1377:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file diff --git a/assets/js/ea907698.52247832.js b/assets/js/ea907698.52247832.js deleted file mode 100644 index 03fc971e..00000000 --- a/assets/js/ea907698.52247832.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9362],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},u=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||h[p]||o;return n?a.createElement(m,r(r({ref:t},u),{},{components:n})):a.createElement(m,r({ref:t},u))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:19.875,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},nextItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],u={toc:c};function h(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},u,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects")),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"))}h.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/ea907698.850a16d9.js b/assets/js/ea907698.850a16d9.js new file mode 100644 index 00000000..1c638b1f --- /dev/null +++ b/assets/js/ea907698.850a16d9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9362],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},u=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||h[p]||o;return n?a.createElement(m,r(r({ref:t},u),{},{components:n})):a.createElement(m,r({ref:t},u))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:19.875,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},nextItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],u={toc:c};function h(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},u,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects")),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"))}h.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.ab748a5c.js b/assets/js/runtime~main.a20e2dde.js similarity index 67% rename from assets/js/runtime~main.ab748a5c.js rename to assets/js/runtime~main.a20e2dde.js index 859dc669..0a990b79 100644 --- a/assets/js/runtime~main.ab748a5c.js +++ b/assets/js/runtime~main.a20e2dde.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,c,f,d,t={},b={};function r(e){var a=b[e];if(void 0!==a)return a.exports;var c=b[e]={id:e,loaded:!1,exports:{}};return t[e].call(c.exports,c,c.exports,r),c.loaded=!0,c.exports}r.m=t,r.c=b,e=[],r.O=(a,c,f,d)=>{if(!c){var t=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](c[o])))?c.splice(o--,1):(b=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var t={};a=a||[null,c({}),c([]),c(c)];for(var b=2&f&&e;"object"==typeof b&&!~a.indexOf(b);b=c(b))Object.getOwnPropertyNames(b).forEach((a=>t[a]=()=>e[a]));return t.default=()=>e,r.d(d,t),d},r.d=(e,a)=>{for(var c in a)r.o(a,c)&&!r.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,c)=>(r.f[c](e,a),a)),[])),r.u=e=>"assets/js/"+({2:"14f3c1c8",53:"935f2afb",366:"d2a399e8",533:"b2b675dd",589:"8845108d",907:"2e8e3662",943:"621e7957",1001:"cd35e36e",1019:"3aded9a5",1131:"fc04de90",1187:"b972506a",1265:"93571253",1337:"af8f0ebc",1353:"5e729dc7",1477:"b2f554cd",1713:"a7023ddc",1867:"dac877fa",1930:"f127536a",1981:"0f5bddc1",2029:"5484f123",2093:"cc670dbc",2125:"3d9c95a4",2182:"379b65ab",2192:"27c1859b",2535:"814f3328",2872:"a581386a",3089:"a6aa9e1f",3409:"1fe9a2e9",3478:"b143667f",3491:"1a7530a6",3575:"79d3ae8c",3608:"9e4087bc",3668:"4067f4ab",4013:"01a85c17",4077:"17da2d17",4078:"4bb443f0",4252:"e28473c3",4569:"98caa824",4636:"39bbf0fd",5586:"8a07c89a",5705:"c206e063",5754:"7302b0ae",5819:"710c3838",5880:"bc5abee9",6103:"ccc49370",6705:"51736f2d",6764:"0a44bc10",6835:"ed26bce9",7018:"7abf8f9a",7038:"2b2237c5",7043:"e4eaf29e",7406:"c0b8e344",7598:"fd0b92f1",7606:"04975d12",7802:"f5222e17",7918:"17896441",8005:"7fe44762",8033:"2fdff385",8392:"15b89b76",8610:"6875c492",8716:"ace589d9",8870:"211a5e1a",9149:"d0636688",9313:"4e20cbbc",9362:"ea907698",9389:"3a5322a7",9460:"8bbcec4e",9476:"91a0ce14",9514:"1be78505",9690:"785487f7",9713:"69404bc7",9768:"e25a597f",9779:"89aeea8d"}[e]||e)+"."+{2:"c2d38792",53:"fe299a75",366:"f2cd1949",533:"dfc96f2c",589:"2180eaa6",907:"15923d7e",943:"d7bb0440",1001:"3b429a26",1019:"23eb1d02",1131:"361ca369",1187:"674d469a",1265:"4d0f9174",1337:"45faf093",1353:"6c60fc64",1477:"b6270e72",1713:"bdc2dd45",1867:"9df0ffd6",1930:"9bad374b",1981:"fcb71fda",2029:"25e1a691",2093:"e72a270a",2125:"81ff592d",2182:"88c9f149",2192:"c53f83ed",2535:"d078356d",2872:"e8f56c7d",3089:"a43c7b7d",3409:"0ade7d18",3478:"42fded62",3491:"3da6caa4",3575:"bd2b094b",3608:"b9ed4ced",3668:"d816d078",4013:"19b879d5",4077:"1b5d5086",4078:"03f6ab47",4252:"2334bafa",4569:"beb8b65e",4636:"1fefebe8",4972:"a74063e3",5586:"fcc2e9e3",5705:"b164a484",5754:"0802cf1f",5819:"7d4d27ad",5880:"6103e696",6048:"2a276e04",6103:"39cf747b",6705:"4beb4c74",6764:"3ce6eaca",6835:"7e1b47c0",7018:"a9521a48",7036:"33f660e9",7038:"85ad8950",7043:"b007fb4c",7406:"049157fb",7598:"bf835747",7606:"591cd21f",7802:"c655db7c",7918:"20aed803",8005:"df9132cb",8033:"7bdff0f7",8392:"c673ecd8",8610:"2aeead72",8716:"eecea6bd",8870:"3f0cda3b",9149:"17cafecb",9313:"72434c71",9362:"52247832",9389:"ac991091",9460:"4215899d",9476:"e2a16295",9514:"f583bd04",9690:"4cc67be0",9713:"c166dae7",9768:"58f4e179",9779:"76841436"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="practica-docs:",r.l=(e,a,c,t)=>{if(f[e])f[e].push(a);else{var b,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{b.onerror=b.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],b.parentNode&&b.parentNode.removeChild(b),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:b}),12e4);b.onerror=l.bind(null,b.onerror),b.onload=l.bind(null,b.onload),o&&document.head.appendChild(b)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",93571253:"1265","14f3c1c8":"2","935f2afb":"53",d2a399e8:"366",b2b675dd:"533","8845108d":"589","2e8e3662":"907","621e7957":"943",cd35e36e:"1001","3aded9a5":"1019",fc04de90:"1131",b972506a:"1187",af8f0ebc:"1337","5e729dc7":"1353",b2f554cd:"1477",a7023ddc:"1713",dac877fa:"1867",f127536a:"1930","0f5bddc1":"1981","5484f123":"2029",cc670dbc:"2093","3d9c95a4":"2125","379b65ab":"2182","27c1859b":"2192","814f3328":"2535",a581386a:"2872",a6aa9e1f:"3089","1fe9a2e9":"3409",b143667f:"3478","1a7530a6":"3491","79d3ae8c":"3575","9e4087bc":"3608","4067f4ab":"3668","01a85c17":"4013","17da2d17":"4077","4bb443f0":"4078",e28473c3:"4252","98caa824":"4569","39bbf0fd":"4636","8a07c89a":"5586",c206e063:"5705","7302b0ae":"5754","710c3838":"5819",bc5abee9:"5880",ccc49370:"6103","51736f2d":"6705","0a44bc10":"6764",ed26bce9:"6835","7abf8f9a":"7018","2b2237c5":"7038",e4eaf29e:"7043",c0b8e344:"7406",fd0b92f1:"7598","04975d12":"7606",f5222e17:"7802","7fe44762":"8005","2fdff385":"8033","15b89b76":"8392","6875c492":"8610",ace589d9:"8716","211a5e1a":"8870",d0636688:"9149","4e20cbbc":"9313",ea907698:"9362","3a5322a7":"9389","8bbcec4e":"9460","91a0ce14":"9476","1be78505":"9514","785487f7":"9690","69404bc7":"9713",e25a597f:"9768","89aeea8d":"9779"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(a,c)=>{var f=r.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var t=r.p+r.u(a),b=new Error;r.l(t,(c=>{if(r.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),t=c&&c.target&&c.target.src;b.message="Loading chunk "+a+" failed.\n("+d+": "+t+")",b.name="ChunkLoadError",b.type=d,b.request=t,f[1](b)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,t=c[0],b=c[1],o=c[2],n=0;if(t.some((a=>0!==e[a]))){for(f in b)r.o(b,f)&&(r.m[f]=b[f]);if(o)var i=o(r)}for(a&&a(c);n{"use strict";var e,a,c,f,d,b={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var c=t[e]={id:e,loaded:!1,exports:{}};return b[e].call(c.exports,c,c.exports,r),c.loaded=!0,c.exports}r.m=b,r.c=t,e=[],r.O=(a,c,f,d)=>{if(!c){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](c[o])))?c.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};a=a||[null,c({}),c([]),c(c)];for(var t=2&f&&e;"object"==typeof t&&!~a.indexOf(t);t=c(t))Object.getOwnPropertyNames(t).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,r.d(d,b),d},r.d=(e,a)=>{for(var c in a)r.o(a,c)&&!r.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,c)=>(r.f[c](e,a),a)),[])),r.u=e=>"assets/js/"+({2:"14f3c1c8",53:"935f2afb",366:"d2a399e8",533:"b2b675dd",589:"8845108d",907:"2e8e3662",943:"621e7957",1001:"cd35e36e",1019:"3aded9a5",1131:"fc04de90",1187:"b972506a",1265:"93571253",1337:"af8f0ebc",1353:"5e729dc7",1477:"b2f554cd",1713:"a7023ddc",1867:"dac877fa",1930:"f127536a",1981:"0f5bddc1",2029:"5484f123",2093:"cc670dbc",2125:"3d9c95a4",2182:"379b65ab",2192:"27c1859b",2535:"814f3328",2872:"a581386a",3089:"a6aa9e1f",3409:"1fe9a2e9",3478:"b143667f",3491:"1a7530a6",3575:"79d3ae8c",3608:"9e4087bc",3668:"4067f4ab",4013:"01a85c17",4077:"17da2d17",4078:"4bb443f0",4252:"e28473c3",4569:"98caa824",4636:"39bbf0fd",5586:"8a07c89a",5705:"c206e063",5754:"7302b0ae",5819:"710c3838",5880:"bc5abee9",6103:"ccc49370",6705:"51736f2d",6764:"0a44bc10",6835:"ed26bce9",7018:"7abf8f9a",7038:"2b2237c5",7043:"e4eaf29e",7406:"c0b8e344",7598:"fd0b92f1",7606:"04975d12",7802:"f5222e17",7918:"17896441",8005:"7fe44762",8033:"2fdff385",8392:"15b89b76",8610:"6875c492",8716:"ace589d9",8870:"211a5e1a",9149:"d0636688",9313:"4e20cbbc",9362:"ea907698",9389:"3a5322a7",9460:"8bbcec4e",9476:"91a0ce14",9514:"1be78505",9690:"785487f7",9713:"69404bc7",9768:"e25a597f",9779:"89aeea8d"}[e]||e)+"."+{2:"c2d38792",53:"fe299a75",366:"bd08fe86",533:"dfc96f2c",589:"2180eaa6",907:"15923d7e",943:"d7bb0440",1001:"3b429a26",1019:"23eb1d02",1131:"361ca369",1187:"674d469a",1265:"ab9737d3",1337:"45faf093",1353:"6c60fc64",1477:"d06d85c3",1713:"bdc2dd45",1867:"9df0ffd6",1930:"9bad374b",1981:"fcb71fda",2029:"25e1a691",2093:"e72a270a",2125:"81ff592d",2182:"88c9f149",2192:"c53f83ed",2535:"d078356d",2872:"e8f56c7d",3089:"a43c7b7d",3409:"0ade7d18",3478:"42fded62",3491:"3da6caa4",3575:"bd2b094b",3608:"b9ed4ced",3668:"d816d078",4013:"19b879d5",4077:"1b5d5086",4078:"03f6ab47",4252:"2334bafa",4569:"beb8b65e",4636:"1fefebe8",4972:"a74063e3",5586:"fcc2e9e3",5705:"b164a484",5754:"0802cf1f",5819:"7d4d27ad",5880:"6103e696",6048:"2a276e04",6103:"39cf747b",6705:"4beb4c74",6764:"3ce6eaca",6835:"7e1b47c0",7018:"a9521a48",7036:"33f660e9",7038:"85ad8950",7043:"b007fb4c",7406:"049157fb",7598:"bf835747",7606:"591cd21f",7802:"c655db7c",7918:"20aed803",8005:"df9132cb",8033:"7bdff0f7",8392:"c673ecd8",8610:"2aeead72",8716:"eecea6bd",8870:"3f0cda3b",9149:"17cafecb",9313:"72434c71",9362:"850a16d9",9389:"ac991091",9460:"4215899d",9476:"e2a16295",9514:"f583bd04",9690:"4cc67be0",9713:"c166dae7",9768:"b2cc51c1",9779:"76841436"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="practica-docs:",r.l=(e,a,c,b)=>{if(f[e])f[e].push(a);else{var t,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",93571253:"1265","14f3c1c8":"2","935f2afb":"53",d2a399e8:"366",b2b675dd:"533","8845108d":"589","2e8e3662":"907","621e7957":"943",cd35e36e:"1001","3aded9a5":"1019",fc04de90:"1131",b972506a:"1187",af8f0ebc:"1337","5e729dc7":"1353",b2f554cd:"1477",a7023ddc:"1713",dac877fa:"1867",f127536a:"1930","0f5bddc1":"1981","5484f123":"2029",cc670dbc:"2093","3d9c95a4":"2125","379b65ab":"2182","27c1859b":"2192","814f3328":"2535",a581386a:"2872",a6aa9e1f:"3089","1fe9a2e9":"3409",b143667f:"3478","1a7530a6":"3491","79d3ae8c":"3575","9e4087bc":"3608","4067f4ab":"3668","01a85c17":"4013","17da2d17":"4077","4bb443f0":"4078",e28473c3:"4252","98caa824":"4569","39bbf0fd":"4636","8a07c89a":"5586",c206e063:"5705","7302b0ae":"5754","710c3838":"5819",bc5abee9:"5880",ccc49370:"6103","51736f2d":"6705","0a44bc10":"6764",ed26bce9:"6835","7abf8f9a":"7018","2b2237c5":"7038",e4eaf29e:"7043",c0b8e344:"7406",fd0b92f1:"7598","04975d12":"7606",f5222e17:"7802","7fe44762":"8005","2fdff385":"8033","15b89b76":"8392","6875c492":"8610",ace589d9:"8716","211a5e1a":"8870",d0636688:"9149","4e20cbbc":"9313",ea907698:"9362","3a5322a7":"9389","8bbcec4e":"9460","91a0ce14":"9476","1be78505":"9514","785487f7":"9690","69404bc7":"9713",e25a597f:"9768","89aeea8d":"9779"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(a,c)=>{var f=r.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var b=r.p+r.u(a),t=new Error;r.l(b,(c=>{if(r.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),b=c&&c.target&&c.target.src;t.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,f[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,b=c[0],t=c[1],o=c[2],n=0;if(b.some((a=>0!==e[a]))){for(f in t)r.o(t,f)&&(r.m[f]=t[f]);if(o)var i=o(r)}for(a&&a(c);n - + - + \ No newline at end of file diff --git a/blog/atom.xml b/blog/atom.xml index cbee2fd9..e4d0f8e6 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -13,7 +13,7 @@ 2023-07-07T11:00:00.000Z - Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

]]>
+ Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

]]>
Yoni Goldberg https://github.com/goldbergyoni @@ -34,7 +34,7 @@ <![CDATA[Which Monorepo is right for a Node.js BACKENDΒ now?]]> monorepo-backend - 2023-07-07T05:31:27.000Z + 2023-07-07T05:38:19.000Z As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we looking at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

Layer 1: Plain old folders to stay on top of your code

With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, quickly adding new components. Consider the alternative with multi-repo approach β€” adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.

This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)β€” it might be all you need. We’ve seen a handful of successful Monorepo solutions without any special tooling.

With that said, some of the newer tools augment this experience with interesting features:

  • Both Turborepo and Nx and also Lerna provide a visual representation of the packages’ dependencies
  • Nx allows β€˜visibility rules’ which is about enforcing who can use what. Consider, a β€˜checkout’ library that should be approached only by the β€˜order Microservice’ β€” deviating from this will result in failure during development (not runtime enforcement)

Nx dependencies graph

  • Nx workspace generator allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing

Layer 2: Tasks and pipeline to build your code efficiently

Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of multiple components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.

Apply some commands over multiple packages

In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together β€” this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled β€” one might wait minutes for a wide test to run. While it’s not always a great practice to rely on wide/E2E tests, it’s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines β€” deeply optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:

  • Parallelization β€” If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking β€” why not parallelize?
  • Smart execution plan β€”Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B β€” naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C’s isolated unit tests while building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved β€” this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement

A modern tool advantage over old Lerna. Taken from Turborepo website

  • Detect who is affected by a change β€” Even on a system with high coupling between packages, it’s usually not necessary to run all packages rather than only those who are affected by a change. What exactly is β€˜affected’? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature β€”developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed
  • Sub-systems (i.e., projects) β€” Similarly to β€˜affected’ above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group
  • Caching β€” This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources β€” if any of these resources haven’t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing β€” consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like β€˜real testing while in fact, the tests did not run at all rather the cache!
  • Remote caching β€” Similarly to caching, only by placing the task’s hashmaps and result on a global server so further executions on other team member’s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time

Layer 3: Hoist your dependencies to boost npm installation

The speed optimizations that were described above won’t be of help if the bottleneck is the big bull of mud that is called β€˜npm install’ (not to criticize, it’s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (the NPM doppelgangers problem) the installation of those packages which might result in a long process.

This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization β€” Instead of installing dependencies inside each component β€˜NODE_MODULES’ folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.

Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.

On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It’s possible to create β€˜publishable’ components that do have a package.json, it’s just not the default.

I’m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can’t do this at the moment? Also, package.json is part of Node.js runtime and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked β€” library B was wadded, I tried to import it from Library A but couldn’t get the β€˜import’ statement to specify the right package name. The natural action was to open B’s package.json and check the name, but there is no Package.json… How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new β€˜framework’.

Stop for a second: It’s all about your workflow

We deal with tooling and features, but it’s actually meaningless evaluating these options before determining whether your preferred workflow is synchronized or independent (we will discuss this in a few seconds). This upfront fundamental decision will change almost everything.

Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?

Option A β€” The synchronized workflow- Going with this development style, all the three components will be developed and deployed in one chunk together. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed together.

Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk’s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.

Option B β€” Independent workflow- This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change β€” The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week β€” deploy it.

Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components β€” some are coded by different teams. This workflow also forces her to write efficient tests against the library β€” it’s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client’s perspective loses some dimension of realism. Also, if a single developer has to update 5 units β€” publishing each individually to the registry and then updating within all the dependencies can be a little tedious.

Synchronized and independent workflows illustrated

On the illusion of synchronicity

In distributed systems, it’s not feasible to achieve 100% synchronicity β€” believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2’s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow β€” The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.

This fundamental decision, synchronized or independent, will determine so many things β€” Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package’s folder, and whether to create a local link between packages which is described in the next paragraph.

Layer 4: Link your packages for immediate feedback

When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:

Option 1: Using npm β€” Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.

Option2: Just a plain folder β€” With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it’s just code in a dedicated folder. This is for example how Nest.js modules are represented.

With option 1, teams benefit from all the great merits of a package manager β€” SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won’t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers β€” one can’t just code over multiple packages and test/run the changes.

With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.

How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time β€” the copy is grabbed from the registry.

Linking packages in a Monorepo

If you’re doing the synchronized workflow, you’re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.

If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a β€˜monolith monorepo’, or maybe a β€˜monolitho’. However, when not linking, it’s harder to debug a small issue between the Microservice and the npm library. What I typically do is temporarily link (with npm link) between the packages, debug, code, then finally remove the link.

Nx is taking a slightly more disruptive approach β€” it is using TypeScript paths to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.

Closing: What should you use?

It’s all about your workflow and architecture β€” a huge unseen cross-road stands in front of the Monorepo tooling decision.

Scenario A β€” If your architecture dictates a synchronized workflow where all packages are deployed together, or at least developed in collaboration β€” then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.

For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you’re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly β€” for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many relatively coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.

If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.

Scenario Bβ€” If you’re into an independent workflow where each package is developed, tested, and deployed (almost) independently β€” then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool β€” Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is Bilt by Gil Tayar, it’s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.

In any scenario, consider workspaces β€” If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you’re working in an autonomous workflow, smaller are the chances of facing such issues. Don’t just use tools unless there is a pain.

We tried to show the beauty of each and where it shines. If we’re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.

Bonus: Comparison table

See below a detailed comparison table of the various tools and features:

Preview only, the complete table can be found here

]]>
diff --git a/blog/index.html b/blog/index.html index 75a5a584..744bd742 100644 --- a/blog/index.html +++ b/blog/index.html @@ -9,13 +9,13 @@ - +
-

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

- +

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

+ \ No newline at end of file diff --git a/blog/is-prisma-better-than-your-traditional-orm/index.html b/blog/is-prisma-better-than-your-traditional-orm/index.html index 88e8c7a6..4bfd2eff 100644 --- a/blog/is-prisma-better-than-your-traditional-orm/index.html +++ b/blog/is-prisma-better-than-your-traditional-orm/index.html @@ -9,7 +9,7 @@ - + @@ -19,7 +19,7 @@ Example: Prisma is not faster than others. It should be noted that in other benchmarks Prisma scores higher and shows an 'average' performance Source

πŸ“Š How important: It's expected from ORM users to live peacefully with inferior performance, for many systems it won't make a great deal. With that, 10%-30% performance differences between various ORMs are not a key factor

Medium importance

πŸ† Is Prisma doing better?: No

4. No active records here!​

πŸ’β€β™‚οΈ What is it about: Node in its early days was heavily inspired by Ruby (e.g., testing "describe"), many great patterns were embraced, Active Record is not among the successful ones. What is this pattern about in a nutshell? say you deal with Orders in your system, with Active Record an Order object/class will hold both the entity properties, possible also some of the logic functions and also CRUD functions. Many find this pattern to be awful, why? ideally, when coding some logic/flow, one should not keep her mind busy with side effects and DB narratives. It also might be that accessing some property unconsciously invokes a heavy DB call (i.e., lazy loading). If not enough, in case of heavy logic, unit tests might be in order (i.e., read 'selective unit tests') - it's going to be much harder to write unit tests against code that interacts with the DB. In fact, all of the respectable and popular architecture (e.g., DDD, clean, 3-tiers, etc) advocate to 'isolate the domain', separate the core/logic of the system from the surrounding technologies. With all of that said, both TypeORM and Sequelize support the Active Record pattern which is displayed in many examples within their documentation. Both also support other better patterns like the data mapper (see below), but they still open the door for doubtful patterns

// TypeORM active records 😟

@Entity()
class Order extends BaseEntity {
@PrimaryGeneratedColumn()
id: number

@Column()
price: number

@ManyToOne(() => Product, (product) => product.order)
products: Product[]

// Other columns here
}

function updateOrder(orderToUpdate: Order){
if(orderToUpdate.price > 100){
// some logic here
orderToUpdate.status = "approval";
orderToUpdate.save();
orderToUpdate.products.forEach((products) =>{

})
orderToUpdate.usedConnection = ?
}
}



πŸ€” How Prisma is different: The better alternative is the data mapper pattern. It acts as a bridge, an adapter, between simple object notations (domain objects with properties) to the DB language, typically SQL. Call it with a plain JS object, POJO, get it saved in the DB. Simple. It won't add functions to the result objects or do anything beyond returning pure data, no surprising side effects. In its purest sense, this is a DB-related utility and completely detached from the business logic. While both Sequelize and TypeORM support this, Prisma offers only this style - no room for mistakes.

// Prisma approach with a data mapper  πŸ‘

// This was generated automatically by Prisma
type Order {
id: number

price: number

products: Product[]

// Other columns here
}

function updateOrder(orderToUpdate: Order){
if(orderToUpdate.price > 100){
orderToUpdate.status = "approval";
prisma.order.update({ where: { id: orderToUpdate.id }, data: orderToUpdate });
// Side effect πŸ‘†, but an explicit one. The thoughtful coder will move this to another function. Since it's happening outside, mocking is possible πŸ‘
products.forEach((products) =>{ // No lazy loading, the data is already here πŸ‘

})
}
}

In Practica.js we take it one step further and put the prisma models within the "DAL" layer and wrap it with the repository pattern. You may glimpse into the code here, this is the business flow that calls the DAL layer

πŸ“Š How important: On the one hand, this is a key architectural principle to follow but the other hand most ORMs allow doing it right

Medium importance

πŸ† Is Prisma doing better?: Yes!

5. Documentation and developer-experience​

πŸ’β€β™‚οΈ What is it about: TypeORM and Sequelize documentation is mediocre, though TypeORM is a little better. Based on my personal experience they do get a little better over the years, but still by no mean they deserve to be called "good" or "great". For example, if you seek to learn about 'raw queries' - Sequelize offers a very short page on this matter, TypeORM info is spread in multiple other pages. Looking to learn about pagination? Couldn't find Sequelize documents, TypeORM has some short explanation, 150 words only

πŸ€” How Prisma is different: Prisma documentation rocks! See their documents on similar topics: raw queries and pagingation, thousands of words, and dozens of code examples. The writing itself is also great, feels like some professional writers were involved

Prisma docs are comprehensive

This chart above shows how comprehensive are Prisma docs (Obviously this by itself doesn't prove quality)

πŸ“Š How important: Great docs are a key to awareness and avoiding pitfalls

Medium importance

πŸ† Is Prisma doing better?: You bet

6. Observability, metrics, and tracing​

πŸ’β€β™‚οΈ What is it about: Good chances are (say about 99.9%) that you'll find yourself diagnostic slow queries in production or any other DB-related quirks. What can you expect from traditional ORMs in terms of observability? Mostly logging. Sequelize provides both logging of query duration and programmatic access to the connection pool state ({size,available,using,waiting}). TypeORM provides only logging of queries that suppress a pre-defined duration threshold. This is better than nothing, but assuming you don't read production logs 24/7, you'd probably need more than logging - an alert to fire when things seem faulty. To achieve this, it's your responsibility to bridge this info to your preferred monitoring system. Another logging downside for this sake is verbosity - we need to emit tons of information to the logs when all we really care for is the average duration. Metrics can serve this purpose much better as we're about to see soon with Prisma

What if you need to dig into which specific part of the query is slow? unfortunately, there is no breakdown of the query phases duration - it's being left to you as a black-box

// Sequelize - logging various DB information

Logging query duration Logging each query in order to realize trends and anomaly in the monitoring system

πŸ€” How Prisma is different: Since Prisma targets also enterprises, it must bring strong ops capabilities. Beautifully, it packs support for both metrics and open telemetry tracing!. For metrics, it generates custom JSON with metric keys and values so anyone can adapt this to any monitoring system (e.g., CloudWatch, statsD, etc). On top of this, it produces out of the box metrics in Prometheus format (one of the most popular monitoring platforms). For example, the metric 'prisma_client_queries_duration_histogram_ms' provides the average query length in the system overtime. What is even more impressive is the support for open-tracing - it feeds your OpenTelemetry collector with spans that describe the various phases of every query. For example, it might help realize what is the bottleneck in the query pipeline: Is it the DB connection, the query itself or the serialization?

prisma tracing Prisma visualizes the various query phases duration with open-telemtry

πŸ† Is Prisma doing better?: Definitely

πŸ“Š How important: Goes without words how impactful is observability, however filling the gap in other ORM will demand no more than a few days

Medium importance

7. Continuity - will it be here with us in 2024/2025​

πŸ’β€β™‚οΈ What is it about: We live quite peacefully with the risk of one of our dependencies to disappear. With ORM though, this risk demand special attention because our buy-in is higher (i.e., harder to replace) and maintaining it was proven to be harder. Just look at a handful of successful ORMs in the past: objection.js, waterline, bookshelf - all of these respectful project had 0 commits in the past month. The single maintainer of objection.js announced that he won't work the project anymore. This high churn rate is not surprising given the huge amount of moving parts to maintain, the gazillion corner cases and the modest 'budget' OSS projects live with. Looking at OpenCollective shows that Sequelize and TypeORM are funded with ~1500$ month in average. This is barely enough to cover a daily Starbucks cappuccino and croissant (6.95$ x 365) for 5 maintainers. Nothing contrasts this model more than a startup company that just raised its series B - Prisma is funded with 40,000,000$ (40 millions) and recruited 80 people! Should not this inspire us with high confidence about their continuity? I'll surprisingly suggest that quite the opposite is true

See, an OSS ORM has to go over one huge hump, but a startup company must pass through TWO. The OSS project will struggle to achieve the critical mass of features, including some high technical barriers (e.g., TypeScript support, ESM). This typically lasts years, but once it does - a project can focus mostly on maintenance and step out of the danger zone. The good news for TypeORM and Sequelize is that they already did! Both struggled to keep their heads above the water, there were rumors in the past that TypeORM is not maintained anymore, but they managed to go through this hump. I counted, both projects had approximately ~2000 PRs in the past 3 years! Going with repo-tracker, each see multiple commits every week. They both have vibrant traction, and the majority of features you would expect from an ORM. TypeORM even supports beyond-the-basics features like multi data source and caching. It's unlikely that now, once they reached the promise land - they will fade away. It might happen, there is no guarantee in the OSS galaxy, but the risk is low

One hump

πŸ€” How Prisma is different: Prisma a little lags behind in terms of features, but with a budget of 40M$ - there are good reasons to believe that they will pass the first hump, achieving a critical mass of features. I'm more concerned with the second hump - showing revenues in 2 years or saying goodbye. As a company that is backed by venture capitals - the model is clear and cruel: In order to secure their next round, series B or C (depends whether the seed is counted), there must be a viable and proven business model. How do you 'sell' ORM? Prisma experiments with multiple products, none is mature yet or being paid for. How big is this risk? According to this startup companies success statistics, "About 65% of the Series A startups get series B, while 35% of the companies that get series A fail.". Since Prisma already gained a lot of love and adoption from the community, there success chances are higher than the average round A/B company, but even 20% or 10% chances to fade away is concerning

This is terrifying news - companies happily choose a young commercial OSS product without realizing that there are 10-30% chances for this product to disappear

Two humps

Some of startup companies who seek a viable business model do not shut the doors rather change the product, the license or the free features. This is not my subjective business analysis, here are few examples: MongoDB changed their license, this is why the majority had to host their Mongo DB over a single vendor. Redis did something similar. What are the chances of Prisma pivoting to another type of product? It actually already happened before, Prisma 1 was mostly about graphQL client and server, it's now retired

It's just fair to mention the other potential path - most round B companies do succeed to qualify for the next round, when this happens even bigger money will be involved in building the 'Ferrari' of JavaScript ORMs. I'm surely crossing my fingers for these great people, at the same time we have to be conscious about our choices

πŸ“Š How important: As important as having to code again the entire DB layer in a big system

Medium importance

πŸ† Is Prisma doing better?: Quite the opposite

Closing - what should you use now?​

Before proposing my key take away - which is the primary ORM, let's repeat the key learning that were introduced here:

  1. πŸ₯‡ Prisma deserves a medal for its awesome DX, documentation, observability support and end-to-end TypeScript coverage
  2. πŸ€” There are reasons to be concerned about Prisma's business continuity as a young startup without a viable business model. Also Prisma's abstract client syntax might blind developers a little more than other ORMs
  3. 🎩 The contenders, TypeORM and Sequelize, matured and doing quite well: both have merged thousand PRs in the past 3 years to become more stable, they keep introducing new releases (see repo-tracker), and for now holds more features than Prisma. Also, both show solid performance (for an ORM). Hats off to the maintainers!

Based on these observations, which should you pick? which ORM will we use for practica.js?

Prisma is an excellent addition to Node.js ORMs family, but not the hassle-free one tool to rule them all. It's a mixed bag of many delicious candies and a few gotchas. Wouldn't it grow to tick all the boxes? Maybe, but unlikely. Once built, it's too hard to dramatically change the syntax and engine performance. Then, during the writing and speaking with the community, including some Prisma enthusiasts, I realized that it doesn't aim to be the can-do-everything 'Ferrari'. Its positioning seems to resemble more a convenient family car with a solid engine and awesome user experience. In other words, it probably aims for the enterprise space where there is mostly demand for great DX, OK performance, and business-class support

In the end of this journey I see no dominant flawless 'Ferrari' ORM. I should probably change my perspective: Building ORM for the hectic modern JavaScript ecosystem is 10x harder than building a Java ORM back then in 2001. There is no stain in the shirt, it's a cool JavaScript swag. I learned to accept what we have, a rich set of features, tolerable performance, good enough for many systems. Need more? Don't use ORM. Nothing is going to change dramatically, it's now as good as it can be

When will it shine?​

Surely use Prisma under these scenarios - If your data needs are rather simple; when time-to-market concern takes precedence over the data processing accuracy; when the DB is relatively small; if you're a mobile/frontend developer who is doing her first steps in the backend world; when there is a need for business-class support; AND when Prisma's long term business continuity risk is a non-issue for you

I'd probably prefer other options under these conditions - If the DB layer performance is a major concern; if you're savvy backend developer with solid SQL capabilities; when there is a need for fine grain control over the data layer. For all of these cases, Prisma might still work, but my primary choices would be using knex/TypeORM/Sequelize with a data-mapper style

Consequently, we love Prisma and add it behind flag (--orm=prisma) to Practica.js. At the same time, until some clouds will disappear, Sequelize will remain our default ORM

- + \ No newline at end of file diff --git a/blog/monorepo-backend/index.html b/blog/monorepo-backend/index.html index 11f8fc82..28ea4502 100644 --- a/blog/monorepo-backend/index.html +++ b/blog/monorepo-backend/index.html @@ -3,19 +3,19 @@ -Which Monorepo is right for a Node.js BACKENDΒ now? | Practica.js +Which Monorepo is right for a Node.js BACKENDΒ now? | Practica.js - +
-

Which Monorepo is right for a Node.js BACKENDΒ now?

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

Layer 1: Plain old folders to stay on top of your code

With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, quickly adding new components. Consider the alternative with multi-repo approach β€” adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.

This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)β€” it might be all you need. We’ve seen a handful of successful Monorepo solutions without any special tooling.

With that said, some of the newer tools augment this experience with interesting features:

  • Both Turborepo and Nx and also Lerna provide a visual representation of the packages’ dependencies
  • Nx allows β€˜visibility rules’ which is about enforcing who can use what. Consider, a β€˜checkout’ library that should be approached only by the β€˜order Microservice’ β€” deviating from this will result in failure during development (not runtime enforcement)

Nx dependencies graph

  • Nx workspace generator allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing

Layer 2: Tasks and pipeline to build your code efficiently

Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of multiple components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.

Apply some commands over multiple packages

In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together β€” this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled β€” one might wait minutes for a wide test to run. While it’s not always a great practice to rely on wide/E2E tests, it’s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines β€” deeply optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:

  • Parallelization β€” If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking β€” why not parallelize?
  • Smart execution plan β€”Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B β€” naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C’s isolated unit tests while building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved β€” this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement

A modern tool advantage over old Lerna. Taken from Turborepo website

  • Detect who is affected by a change β€” Even on a system with high coupling between packages, it’s usually not necessary to run all packages rather than only those who are affected by a change. What exactly is β€˜affected’? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature β€”developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed
  • Sub-systems (i.e., projects) β€” Similarly to β€˜affected’ above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group
  • Caching β€” This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources β€” if any of these resources haven’t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing β€” consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like β€˜real testing while in fact, the tests did not run at all rather the cache!
  • Remote caching β€” Similarly to caching, only by placing the task’s hashmaps and result on a global server so further executions on other team member’s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time

Layer 3: Hoist your dependencies to boost npm installation

The speed optimizations that were described above won’t be of help if the bottleneck is the big bull of mud that is called β€˜npm install’ (not to criticize, it’s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (the NPM doppelgangers problem) the installation of those packages which might result in a long process.

This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization β€” Instead of installing dependencies inside each component β€˜NODE_MODULES’ folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.

Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.

On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It’s possible to create β€˜publishable’ components that do have a package.json, it’s just not the default.

I’m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can’t do this at the moment? Also, package.json is part of Node.js runtime and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked β€” library B was wadded, I tried to import it from Library A but couldn’t get the β€˜import’ statement to specify the right package name. The natural action was to open B’s package.json and check the name, but there is no Package.json… How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new β€˜framework’.

Stop for a second: It’s all about your workflow

We deal with tooling and features, but it’s actually meaningless evaluating these options before determining whether your preferred workflow is synchronized or independent (we will discuss this in a few seconds). This upfront fundamental decision will change almost everything.

Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?

Option A β€” The synchronized workflow- Going with this development style, all the three components will be developed and deployed in one chunk together. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed together.

Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk’s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.

Option B β€” Independent workflow- This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change β€” The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week β€” deploy it.

Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components β€” some are coded by different teams. This workflow also forces her to write efficient tests against the library β€” it’s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client’s perspective loses some dimension of realism. Also, if a single developer has to update 5 units β€” publishing each individually to the registry and then updating within all the dependencies can be a little tedious.

Synchronized and independent workflows illustrated

On the illusion of synchronicity

In distributed systems, it’s not feasible to achieve 100% synchronicity β€” believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2’s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow β€” The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.

This fundamental decision, synchronized or independent, will determine so many things β€” Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package’s folder, and whether to create a local link between packages which is described in the next paragraph.

Layer 4: Link your packages for immediate feedback

When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:

Option 1: Using npm β€” Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.

Option2: Just a plain folder β€” With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it’s just code in a dedicated folder. This is for example how Nest.js modules are represented.

With option 1, teams benefit from all the great merits of a package manager β€” SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won’t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers β€” one can’t just code over multiple packages and test/run the changes.

With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.

How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time β€” the copy is grabbed from the registry.

Linking packages in a Monorepo

If you’re doing the synchronized workflow, you’re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.

If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a β€˜monolith monorepo’, or maybe a β€˜monolitho’. However, when not linking, it’s harder to debug a small issue between the Microservice and the npm library. What I typically do is temporarily link (with npm link) between the packages, debug, code, then finally remove the link.

Nx is taking a slightly more disruptive approach β€” it is using TypeScript paths to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.

Closing: What should you use?

It’s all about your workflow and architecture β€” a huge unseen cross-road stands in front of the Monorepo tooling decision.

Scenario A β€” If your architecture dictates a synchronized workflow where all packages are deployed together, or at least developed in collaboration β€” then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.

For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you’re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly β€” for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many relatively coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.

If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.

Scenario Bβ€” If you’re into an independent workflow where each package is developed, tested, and deployed (almost) independently β€” then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool β€” Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is Bilt by Gil Tayar, it’s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.

In any scenario, consider workspaces β€” If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you’re working in an autonomous workflow, smaller are the chances of facing such issues. Don’t just use tools unless there is a pain.

We tried to show the beauty of each and where it shines. If we’re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.

Bonus: Comparison table

See below a detailed comparison table of the various tools and features:

Preview only, the complete table can be found here

- +

Which Monorepo is right for a Node.js BACKENDΒ now?

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

Layer 1: Plain old folders to stay on top of your code

With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, quickly adding new components. Consider the alternative with multi-repo approach β€” adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.

This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)β€” it might be all you need. We’ve seen a handful of successful Monorepo solutions without any special tooling.

With that said, some of the newer tools augment this experience with interesting features:

  • Both Turborepo and Nx and also Lerna provide a visual representation of the packages’ dependencies
  • Nx allows β€˜visibility rules’ which is about enforcing who can use what. Consider, a β€˜checkout’ library that should be approached only by the β€˜order Microservice’ β€” deviating from this will result in failure during development (not runtime enforcement)

Nx dependencies graph

  • Nx workspace generator allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing

Layer 2: Tasks and pipeline to build your code efficiently

Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of multiple components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.

Apply some commands over multiple packages

In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together β€” this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled β€” one might wait minutes for a wide test to run. While it’s not always a great practice to rely on wide/E2E tests, it’s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines β€” deeply optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:

  • Parallelization β€” If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking β€” why not parallelize?
  • Smart execution plan β€”Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B β€” naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C’s isolated unit tests while building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved β€” this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement

A modern tool advantage over old Lerna. Taken from Turborepo website

  • Detect who is affected by a change β€” Even on a system with high coupling between packages, it’s usually not necessary to run all packages rather than only those who are affected by a change. What exactly is β€˜affected’? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature β€”developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed
  • Sub-systems (i.e., projects) β€” Similarly to β€˜affected’ above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group
  • Caching β€” This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources β€” if any of these resources haven’t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing β€” consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like β€˜real testing while in fact, the tests did not run at all rather the cache!
  • Remote caching β€” Similarly to caching, only by placing the task’s hashmaps and result on a global server so further executions on other team member’s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time

Layer 3: Hoist your dependencies to boost npm installation

The speed optimizations that were described above won’t be of help if the bottleneck is the big bull of mud that is called β€˜npm install’ (not to criticize, it’s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (the NPM doppelgangers problem) the installation of those packages which might result in a long process.

This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization β€” Instead of installing dependencies inside each component β€˜NODE_MODULES’ folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.

Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.

On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It’s possible to create β€˜publishable’ components that do have a package.json, it’s just not the default.

I’m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can’t do this at the moment? Also, package.json is part of Node.js runtime and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked β€” library B was wadded, I tried to import it from Library A but couldn’t get the β€˜import’ statement to specify the right package name. The natural action was to open B’s package.json and check the name, but there is no Package.json… How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new β€˜framework’.

Stop for a second: It’s all about your workflow

We deal with tooling and features, but it’s actually meaningless evaluating these options before determining whether your preferred workflow is synchronized or independent (we will discuss this in a few seconds). This upfront fundamental decision will change almost everything.

Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?

Option A β€” The synchronized workflow- Going with this development style, all the three components will be developed and deployed in one chunk together. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed together.

Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk’s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.

Option B β€” Independent workflow- This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change β€” The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week β€” deploy it.

Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components β€” some are coded by different teams. This workflow also forces her to write efficient tests against the library β€” it’s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client’s perspective loses some dimension of realism. Also, if a single developer has to update 5 units β€” publishing each individually to the registry and then updating within all the dependencies can be a little tedious.

Synchronized and independent workflows illustrated

On the illusion of synchronicity

In distributed systems, it’s not feasible to achieve 100% synchronicity β€” believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2’s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow β€” The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.

This fundamental decision, synchronized or independent, will determine so many things β€” Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package’s folder, and whether to create a local link between packages which is described in the next paragraph.

Layer 4: Link your packages for immediate feedback

When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:

Option 1: Using npm β€” Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.

Option2: Just a plain folder β€” With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it’s just code in a dedicated folder. This is for example how Nest.js modules are represented.

With option 1, teams benefit from all the great merits of a package manager β€” SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won’t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers β€” one can’t just code over multiple packages and test/run the changes.

With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.

How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time β€” the copy is grabbed from the registry.

Linking packages in a Monorepo

If you’re doing the synchronized workflow, you’re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.

If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a β€˜monolith monorepo’, or maybe a β€˜monolitho’. However, when not linking, it’s harder to debug a small issue between the Microservice and the npm library. What I typically do is temporarily link (with npm link) between the packages, debug, code, then finally remove the link.

Nx is taking a slightly more disruptive approach β€” it is using TypeScript paths to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.

Closing: What should you use?

It’s all about your workflow and architecture β€” a huge unseen cross-road stands in front of the Monorepo tooling decision.

Scenario A β€” If your architecture dictates a synchronized workflow where all packages are deployed together, or at least developed in collaboration β€” then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.

For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you’re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly β€” for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many relatively coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.

If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.

Scenario Bβ€” If you’re into an independent workflow where each package is developed, tested, and deployed (almost) independently β€” then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool β€” Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is Bilt by Gil Tayar, it’s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.

In any scenario, consider workspaces β€” If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you’re working in an autonomous workflow, smaller are the chances of facing such issues. Don’t just use tools unless there is a pain.

We tried to show the beauty of each and where it shines. If we’re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.

Bonus: Comparison table

See below a detailed comparison table of the various tools and features:

Preview only, the complete table can be found here

+ \ No newline at end of file diff --git a/blog/popular-nodejs-pattern-and-tools-to-reconsider/index.html b/blog/popular-nodejs-pattern-and-tools-to-reconsider/index.html index a7d17c70..165f3155 100644 --- a/blog/popular-nodejs-pattern-and-tools-to-reconsider/index.html +++ b/blog/popular-nodejs-pattern-and-tools-to-reconsider/index.html @@ -9,13 +9,13 @@ - +

Popular Node.js patterns and tools to re-consider

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

1. Dotenv as your configuration source​

πŸ’β€β™‚οΈ What is it about: A super popular technique in which the app configurable values (e.g., DB user name) are stored in a simple text file. Then, when the app loads, the dotenv library sets all the text file values as environment variables so the code can read this

// .env file
USER_SERVICE_URL=https://users.myorg.com

//start.js
require('dotenv').config();

//blog-post-service.js
repository.savePost(post);
//update the user number of posts, read the users service URL from an environment variable
await axios.put(`${process.env.USER_SERVICE_URL}/api/user/${post.userId}/incrementPosts`)

πŸ“Š How popular: 21,806,137 downloads/week!

πŸ€” Why it might be wrong: Dotenv is so easy and intuitive to start with, so one might easily overlook fundamental features: For example, it's hard to infer the configuration schema and realize the meaning of each key and its typing. Consequently, there is no built-in way to fail fast when a mandatory key is missing - a flow might fail after starting and presenting some side effects (e.g., DB records were already mutated before the failure). In the example above, the blog post will be saved to DB, and only then will the code realize that a mandatory key is missing - This leaves the app hanging in an invalid state. On top of this, in the presence of many keys, it's impossible to organize them hierarchically. If not enough, it encourages developers to commit this .env file which might contain production values - this happens because there is no clear way to define development defaults. Teams usually work around this by committing .env.example file and then asking whoever pulls code to rename this file manually. If they remember to of course

β˜€οΈ Better alternative: Some configuration libraries provide out of the box solution to all of these needs. They encourage a clear schema and the possibility to validate early and fail if needed. See comparison of options here. One of the better alternatives is 'convict', down below is the same example, this time with Convict, hopefully it's better now:

// config.js
export default {
userService: {
url: {
// Hierarchical, documented and strongly typed πŸ‘‡
doc: "The URL of the user management service including a trailing slash",
format: "url",
default: "http://localhost:4001",
nullable: false,
env: "USER_SERVICE_URL",
},
},
//more keys here
};

//start.js
import convict from "convict";
import configSchema from "config";
convict(configSchema);
// Fail fast!
convictConfigurationProvider.validate();

//blog-post.js
repository.savePost(post);
// Will never arrive here if the URL is not set
await axios.put(
`${convict.get(userService.url)}/api/user/${post.userId}/incrementPosts`
);

2. Calling a 'fat' service from the API controller​

πŸ’β€β™‚οΈ What is it about: Consider a reader of our code who wishes to understand the entire high-level flow or delve into a very specific part. She first lands on the API controller, where requests start. Unlike what its name implies, this controller layer is just an adapter and kept really thin and straightforward. Great thus far. Then the controller calls a big 'service' with thousands of lines of code that represent the entire logic

// user-controller
router.post('/', async (req, res, next) => {
await userService.add(req.body);
// Might have here try-catch or error response logic
}

// user-service
exports function add(newUser){
// Want to understand quickly? Need to understand the entire user service, 1500 loc
// It uses technical language and reuse narratives of other flows
this.copyMoreFieldsToUser(newUser)
const doesExist = this.updateIfAlreadyExists(newUser)
if(!doesExist){
addToCache(newUser);
}
// 20 more lines that demand navigating to other functions in order to get the intent
}


πŸ“Š How popular: It's hard to pull solid numbers here, I could confidently say that in most of the app that I see, this is the case

πŸ€” Why it might be wrong: We're here to tame complexities. One of the useful techniques is deferring a complexity to the later stage possible. In this case though, the reader of the code (hopefully) starts her journey through the tests and the controller - things are simple in these areas. Then, as she lands on the big service - she gets tons of complexity and small details, although she is focused on understanding the overall flow or some specific logic. This is unnecessary complexity

β˜€οΈ Better alternative: The controller should call a particular type of service, a use-case , which is responsible for summarizing the flow in a business and simple language. Each flow/feature is described using a use-case, each contains 4-10 lines of code, that tell the story without technical details. It mostly orchestrates other small services, clients, and repositories that hold all the implementation details. With use cases, the reader can grasp the high-level flow easily. She can now choose where she would like to focus. She is now exposed only to necessary complexity. This technique also encourages partitioning the code to the smaller object that the use-case orchestrates. Bonus: By looking at coverage reports, one can tell which features are covered, not just files/functions

This idea by the way is formalized in the 'clean architecture' book - I'm not a big fan of 'fancy' architectures, but see - it's worth cherry-picking techniques from every source. You may walk-through our Node.js best practices starter, practica.js, and examine the use-cases code

// add-order-use-case.js
export async function addOrder(newOrder: addOrderDTO) {
orderValidation.assertOrderIsValid(newOrder);
const userWhoOrdered = await userServiceClient.getUserWhoOrdered(
newOrder.userId
);
paymentTermsService.assertPaymentTerms(
newOrder.paymentTermsInDays,
userWhoOrdered.terms
);

const response = await orderRepository.addOrder(newOrder);

return response;
}

3. Nest.js: Wire everything with dependency injection​

πŸ’β€β™‚οΈ What is it about: If you're doing Nest.js, besides having a powerful framework in your hands, you probably use DI for everything and make every class injectable. Say you have a weather-service that depends upon humidity-service, and there is no requirement to swap the humidity-service with alternative providers. Nevertheless, you inject humidity-service into the weather-service. It becomes part of your development style, "why not" you think - I may need to stub it during testing or replace it in the future

// humidity-service.ts - not customer facing
@Injectable()
export class GoogleHumidityService {

async getHumidity(when: Datetime): Promise<number> {
// Fetches from some specific cloud service
}
}

// weather-service.ts - customer facing
import { GoogleHumidityService } from './humidity-service.ts';

export type weatherInfo{
temperature: number,
humidity: number
}

export class WeatherService {
constructor(private humidityService: GoogleHumidityService) {}

async GetWeather(when: Datetime): Promise<weatherInfo> {
// Fetch temperature from somewhere and then humidity from GoogleHumidityService
}
}

// app.module.ts
@Module({
providers: [GoogleHumidityService, WeatherService],
})
export class AppModule {}

πŸ“Š How popular: No numbers here but I could confidently say that in all of the Nest.js app that I've seen, this is the case. In the popular 'nestjs-realworld-example-ap[p']() all the services are 'injectable'

πŸ€” Why it might be wrong: Dependency injection is not a priceless coding style but a pattern you should pull in the right moment, like any other pattern. Why? Because any pattern has a price. What price, you ask? First, encapsulation is violated. Clients of the weather-service are now aware that other providers are being used internally. Some clients may get tempted to override providers also it's not under their responsibility. Second, it's another layer of complexity to learn, maintain, and one more way to shoot yourself in the legs. StackOverflow owes some of its revenues to Nest.js DI - plenty of discussions try to solve this puzzle (e.g. did you know that in case of circular dependencies the order of imports matters?). Third, there is the performance thing - Nest.js, for example struggled to provide a decent start time for serverless environments and had to introduce lazy loaded modules. Don't get me wrong, in some cases, there is a good case for DI: When a need arises to decouple a dependency from its caller, or to allow clients to inject custom implementations (e.g., the strategy pattern). In such case, when there is a value, you may consider whether the value of DI is worth its price. If you don't have this case, why pay for nothing?

I recommend reading the first paragraphs of this blog post 'Dependency Injection is EVIL' (and absolutely don't agree with this bold words)

β˜€οΈ Better alternative: 'Lean-ify' your engineering approach - avoid using any tool unless it serves a real-world need immediately. Start simple, a dependent class should simply import its dependency and use it - Yeah, using the plain Node.js module system ('require'). Facing a situation when there is a need to factor dynamic objects? There are a handful of simple patterns, simpler than DI, that you should consider, like 'if/else', factory function, and more. Are singletons requested? Consider techniques with lower costs like the module system with factory function. Need to stub/mock for testing? Monkey patching might be better than DI: better clutter your test code a bit than clutter your production code. Have a strong need to hide from an object where its dependencies are coming from? You sure? Use DI!

// humidity-service.ts - not customer facing
export async function getHumidity(when: Datetime): Promise<number> {
// Fetches from some specific cloud service
}

// weather-service.ts - customer facing
import { getHumidity } from "./humidity-service.ts";

// βœ… No wiring is happening externally, all is flat and explicit. Simple
export async function getWeather(when: Datetime): Promise<number> {
// Fetch temperature from somewhere and then humidity from GoogleHumidityService
// Nobody needs to know about it, its an implementation details
await getHumidity(when);
}

1 min pause: A word or two about me, the author​

My name is Yoni Goldberg, I'm a Node.js developer and consultant. I wrote few code-books like JavaScript testing best practices and Node.js best practices (100,000 stars ✨πŸ₯Ή). That said, my best guide is Node.js testing practices which only few read 😞. I shall release an advanced Node.js testing course soon and also hold workshops for teams. I'm also a core maintainer of Practica.js which is a Node.js starter that creates a production-ready example Node Monorepo solution that is based on the standards and simplicity. It might be your primary option when starting a new Node.js solution


4. Passport.js for token authentication​

πŸ’β€β™‚οΈ What is it about: Commonly, you're in need to issue or/and authenticate JWT tokens. Similarly, you might need to allow login from one single social network like Google/Facebook. When faced with these kinds of needs, Node.js developers rush to the glorious library Passport.js like butterflies are attracted to light

πŸ“Š How popular: 1,389,720 weekly downloads

πŸ€” Why it might be wrong: When tasked with guarding your routes with JWT token - you're just a few lines of code shy from ticking the goal. Instead of messing up with a new framework, instead of introducing levels of indirections (you call passport, then it calls you), instead of spending time learning new abstractions - use a JWT library directly. Libraries like jsonwebtoken or fast-jwt are simple and well maintained. Have concerns with the security hardening? Good point, your concerns are valid. But would you not get better hardening with a direct understanding of your configuration and flow? Will hiding things behind a framework help? Even if you prefer the hardening of a battle-tested framework, Passport doesn't handle a handful of security risks like secrets/token, secured user management, DB protection, and more. My point, you probably anyway need fully-featured user and authentication management platforms. Various cloud services and OSS projects, can tick all of those security concerns. Why then start in the first place with a framework that doesn't satisfy your security needs? It seems like many who opt for Passport.js are not fully aware of which needs are satisfied and which are left open. All of that said, Passport definitely shines when looking for a quick way to support many social login providers

β˜€οΈ Better alternative: Is token authentication in order? These few lines of code below might be all you need. You may also glimpse into Practica.js wrapper around these libraries. A real-world project at scale typically need more: supporting async JWT (JWKS), securely manage and rotate the secrets to name a few examples. In this case, OSS solution like [keycloak (https://github.com/keycloak/keycloak) or commercial options like Auth0[https://github.com/auth0] are alternatives to consider

// jwt-middleware.js, a simplified version - Refer to Practica.js to see some more corner cases
const middleware = (req, res, next) => {
if(!req.headers.authorization){
res.sendStatus(401)
}

jwt.verify(req.headers.authorization, options.secret, (err: any, jwtContent: any) => {
if (err) {
return res.sendStatus(401);
}

req.user = jwtContent.data;

next();
});

5. Supertest for integration/API testing​

πŸ’β€β™‚οΈ What is it about: When testing against an API (i.e., component, integration, E2E tests), the library supertest provides a sweet syntax that can both detect the web server address, make HTTP call and also assert on the response. Three in one

test("When adding invalid user, then the response is 400", (done) => {
const request = require("supertest");
const app = express();
// Arrange
const userToAdd = {
name: undefined,
};

// Act
request(app)
.post("/user")
.send(userToAdd)
.expect("Content-Type", /json/)
.expect(400, done);

// Assert
// We already asserted above ☝🏻 as part of the request
});

πŸ“Š How popular: 2,717,744 weekly downloads

πŸ€” Why it might be wrong: You already have your assertion library (Jest? Chai?), it has a great error highlighting and comparison - you trust it. Why code some tests using another assertion syntax? Not to mention, Supertest's assertion errors are not as descriptive as Jest and Chai. It's also cumbersome to mix HTTP client + assertion library instead of choosing the best for each mission. Speaking of the best, there are more standard, popular, and better-maintained HTTP clients (like fetch, axios and other friends). Need another reason? Supertest might encourage coupling the tests to Express as it offers a constructor that gets an Express object. This constructor infers the API address automatically (useful when using dynamic test ports). This couples the test to the implementation and won't work in the case where you wish to run the same tests against a remote process (the API doesn't live with the tests). My repository 'Node.js testing best practices' holds examples of how tests can infer the API port and address

β˜€οΈ Better alternative: A popular and standard HTTP client library like Node.js Fetch or Axios. In Practica.js (a Node.js starter that packs many best practices) we use Axios. It allows us to configure a HTTP client that is shared among all the tests: We bake inside a JWT token, headers, and a base URL. Another good pattern that we look at, is making each Microservice generate HTTP client library for its consumers. This brings strong-type experience to the clients, synchronizes the provider-consumer versions and as a bonus - The provider can test itself with the same library that its consumers are using

test("When adding invalid user, then the response is 400 and includes a reason", (done) => {
const app = express();
// Arrange
const userToAdd = {
name: undefined,
};

// Act
const receivedResponse = axios.post(
`http://localhost:${apiPort}/user`,
userToAdd
);

// Assert
// βœ… Assertion happens in a dedicated stage and a dedicated library
expect(receivedResponse).toMatchObject({
status: 400,
data: {
reason: "no-name",
},
});
});

6. Fastify decorate for non request/web utilities​

πŸ’β€β™‚οΈ What is it about: Fastify introduces great patterns. Personally, I highly appreciate how it preserves the simplicity of Express while bringing more batteries. One thing that got me wondering is the 'decorate' feature which allows placing common utilities/services inside a widely accessible container object. I'm referring here specifically to the case where a cross-cutting concern utility/service is being used. Here is an example:

// An example of a utility that is cross-cutting-concern. Could be logger or anything else
fastify.decorate('metricsService', function (name) {
fireMetric: () => {
// My code that sends metrics to the monitoring system
}
})

fastify.get('/api/orders', async function (request, reply) {
this.metricsService.fireMetric({name: 'new-request'})
// Handle the request
})

// my-business-logic.js
exports function calculateSomething(){
// How to fire a metric?
}

It should be noted that 'decoration' is also used to place values (e.g., user) inside a request - this is a slightly different case and a sensible one

πŸ“Š How popular: Fastify has 696,122 weekly download and growing rapidly. The decorator concept is part of the framework's core

πŸ€” Why it might be wrong: Some services and utilities serve cross-cutting-concern needs and should be accessible from other layers like domain (i.e, business logic, DAL). When placing utilities inside this object, the Fastify object might not be accessible to these layers. You probably don't want to couple your web framework with your business logic: Consider that some of your business logic and repositories might get invoked from non-REST clients like CRON, MQ, and similar - In these cases, Fastify won't get involved at all so better not trust it to be your service locator

β˜€οΈ Better alternative: A good old Node.js module is a standard way to expose and consume functionality. Need a singleton? Use the module system caching. Need to instantiate a service in correlation with a Fastify life-cycle hook (e.g., DB connection on start)? Call it from that Fastify hook. In the rare case where a highly dynamic and complex instantiation of dependencies is needed - DI is also a (complex) option to consider

// βœ… A simple usage of good old Node.js modules
// metrics-service.js

exports async function fireMetric(name){
// My code that sends metrics to the monitoring system
}

import {fireMetric} from './metrics-service.js'

fastify.get('/api/orders', async function (request, reply) {
metricsService.fireMetric({name: 'new-request'})
})

// my-business-logic.js
exports function calculateSomething(){
metricsService.fireMetric({name: 'new-request'})
}

7. Logging from a catch clause​

πŸ’β€β™‚οΈ What is it about: You catch an error somewhere deep in the code (not on the route level), then call logger.error to make this error observable. Seems simple and necessary

try{
axios.post('https://thatService.io/api/users);
}
catch(error){
logger.error(error, this, {operation: addNewOrder});
}

πŸ“Š How popular: Hard to put my hands on numbers but it's quite popular, right?

πŸ€” Why it might be wrong: First, errors should get handled/logged in a central location. Error handling is a critical path. Various catch clauses are likely to behave differently without a centralized and unified behavior. For example, a request might arise to tag all errors with certain metadata, or on top of logging, to also fire a monitoring metric. Applying these requirements in ~100 locations is not a walk in the park. Second, catch clauses should be minimized to particular scenarios. By default, the natural flow of an error is bubbling down to the route/entry-point - from there, it will get forwarded to the error handler. Catch clauses are more verbose and error-prone - therefore it should serve two very specific needs: When one wishes to change the flow based on the error or enrich the error with more information (which is not the case in this example)

β˜€οΈ Better alternative: By default, let the error bubble down the layers and get caught by the entry-point global catch (e.g., Express error middleware). In cases when the error should trigger a different flow (e.g., retry) or there is value in enriching the error with more context - use a catch clause. In this case, ensure the .catch code also reports to the error handler

// A case where we wish to retry upon failure
try{
axios.post('https://thatService.io/api/users);
}
catch(error){
// βœ… A central location that handles error
errorHandler.handle(error, this, {operation: addNewOrder});
callTheUserService(numOfRetries++);
}

8. Use Morgan logger for express web requests​

πŸ’β€β™‚οΈ What is it about: In many web apps, you are likely to find a pattern that is being copy-pasted for ages - Using Morgan logger to log requests information:

const express = require("express");
const morgan = require("morgan");

const app = express();

app.use(morgan("combined"));

πŸ“Š How popular: 2,901,574 downloads/week

πŸ€” Why it might be wrong: Wait a second, you already have your main logger, right? Is it Pino? Winston? Something else? Great. Why deal with and configure yet another logger? I do appreciate the HTTP domain-specific language (DSL) of Morgan. The syntax is sweet! But does it justify having two loggers?

β˜€οΈ Better alternative: Put your chosen logger in a middleware and log the desired request/response properties:

// βœ… Use your preferred logger for all the tasks
const logger = require("pino")();
app.use((req, res, next) => {
res.on("finish", () => {
logger.info(`${req.url} ${res.statusCode}`); // Add other properties here
});
next();
});

9. Having conditional code based on NODE_ENV value​

πŸ’β€β™‚οΈ What is it about: To differentiate between development vs production configuration, it's common to set the environment variable NODE_ENV with "production|test". Doing so allows the various tooling to act differently. For example, some templating engines will cache compiled templates only in production. Beyond tooling, custom applications use this to specify behaviours that are unique to the development or production environment:

if (process.env.NODE_ENV === "production") {
// This is unlikely to be tested since test runner usually set NODE_ENV=test
setLogger({ stdout: true, prettyPrint: false });
// If this code branch above exists, why not add more production-only configurations:
collectMetrics();
} else {
setLogger({ splunk: true, prettyPrint: true });
}

πŸ“Š How popular: 5,034,323 code results in GitHub when searching for "NODE_ENV". It doesn't seem like a rare pattern

πŸ€” Why it might be wrong: Anytime your code checks whether it's production or not, this branch won't get hit by default in some test runner (e.g., Jest set NODE_ENV=test). In any test runner, the developer must remember to test for each possible value of this environment variable. In the example above, collectMetrics() will be tested for the first time in production. Sad smiley. Additionally, putting these conditions opens the door to add more differences between production and the developer machine - when this variable and conditions exists, a developer gets tempted to put some logic for production only. Theoretically, this can be tested: one can set NODE_ENV = "production" in testing and cover the production branches (if she remembers...). But then, if you can test with NODE_ENV='production', what's the point in separating? Just consider everything to be 'production' and avoid this error-prone mental load

β˜€οΈ Better alternative: Any code that was written by us, must be tested. This implies avoiding any form of if(production)/else(development) conditions. Wouldn't anyway developers machine have different surrounding infrastructure than production (e.g., logging system)? They do, the environments are quite difference, but we feel comfortable with it. These infrastructural things are battle-tested, extraneous, and not part of our code. To keep the same code between dev/prod and still use different infrastructure - we put different values in the configuration (not in the code). For example, a typical logger emits JSON in production but in a development machine it emits 'pretty-print' colorful lines. To meet this, we set ENV VAR that tells whether what logging style we aim for:

//package.json
"scripts": {
"start": "LOG_PRETTY_PRINT=false index.js",
"test": "LOG_PRETTY_PRINT=true jest"
}

//index.js
//βœ… No condition, same code for all the environments. The variations are defined externally in config or deployment files
setLogger({prettyPrint: process.env.LOG_PRETTY_PRINT})

Closing​

I hope that these thoughts, at least one of them, made you re-consider adding a new technique to your toolbox. In any case, let's keep our community vibrant, disruptive and kind. Respectful discussions are almost as important as the event loop. Almost.

- + \ No newline at end of file diff --git a/blog/practica-is-alive/index.html b/blog/practica-is-alive/index.html index ff5c50fa..742eb842 100644 --- a/blog/practica-is-alive/index.html +++ b/blog/practica-is-alive/index.html @@ -9,13 +9,13 @@ - +

Practica.js v0.0.1 is alive

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

- + \ No newline at end of file diff --git a/blog/practica-v0.0.6-is-alive/index.html b/blog/practica-v0.0.6-is-alive/index.html index 6726c14c..a3707504 100644 --- a/blog/practica-v0.0.6-is-alive/index.html +++ b/blog/practica-v0.0.6-is-alive/index.html @@ -9,13 +9,13 @@ - +

Practica v0.0.6 is alive

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

- + \ No newline at end of file diff --git a/blog/rss.xml b/blog/rss.xml index c7bc0467..7128bff4 100644 --- a/blog/rss.xml +++ b/blog/rss.xml @@ -14,7 +14,7 @@ testing-the-dark-scenarios-of-your-nodejs-application Fri, 07 Jul 2023 11:00:00 GMT - Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

]]>
+ Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

]]>
node.js testing component-test @@ -27,7 +27,7 @@ <![CDATA[Which Monorepo is right for a Node.js BACKENDΒ now?]]> https://practica.dev/blog/monorepo-backend monorepo-backend - Fri, 07 Jul 2023 05:31:27 GMT + Fri, 07 Jul 2023 05:38:19 GMT As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we looking at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

Layer 1: Plain old folders to stay on top of your code

With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, quickly adding new components. Consider the alternative with multi-repo approach β€” adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.

This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)β€” it might be all you need. We’ve seen a handful of successful Monorepo solutions without any special tooling.

With that said, some of the newer tools augment this experience with interesting features:

  • Both Turborepo and Nx and also Lerna provide a visual representation of the packages’ dependencies
  • Nx allows β€˜visibility rules’ which is about enforcing who can use what. Consider, a β€˜checkout’ library that should be approached only by the β€˜order Microservice’ β€” deviating from this will result in failure during development (not runtime enforcement)

Nx dependencies graph

  • Nx workspace generator allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing

Layer 2: Tasks and pipeline to build your code efficiently

Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of multiple components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.

Apply some commands over multiple packages

In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together β€” this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled β€” one might wait minutes for a wide test to run. While it’s not always a great practice to rely on wide/E2E tests, it’s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines β€” deeply optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:

  • Parallelization β€” If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking β€” why not parallelize?
  • Smart execution plan β€”Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B β€” naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C’s isolated unit tests while building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved β€” this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement

A modern tool advantage over old Lerna. Taken from Turborepo website

  • Detect who is affected by a change β€” Even on a system with high coupling between packages, it’s usually not necessary to run all packages rather than only those who are affected by a change. What exactly is β€˜affected’? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature β€”developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed
  • Sub-systems (i.e., projects) β€” Similarly to β€˜affected’ above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group
  • Caching β€” This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources β€” if any of these resources haven’t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing β€” consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like β€˜real testing while in fact, the tests did not run at all rather the cache!
  • Remote caching β€” Similarly to caching, only by placing the task’s hashmaps and result on a global server so further executions on other team member’s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time

Layer 3: Hoist your dependencies to boost npm installation

The speed optimizations that were described above won’t be of help if the bottleneck is the big bull of mud that is called β€˜npm install’ (not to criticize, it’s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (the NPM doppelgangers problem) the installation of those packages which might result in a long process.

This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization β€” Instead of installing dependencies inside each component β€˜NODE_MODULES’ folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.

Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.

On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It’s possible to create β€˜publishable’ components that do have a package.json, it’s just not the default.

I’m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can’t do this at the moment? Also, package.json is part of Node.js runtime and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked β€” library B was wadded, I tried to import it from Library A but couldn’t get the β€˜import’ statement to specify the right package name. The natural action was to open B’s package.json and check the name, but there is no Package.json… How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new β€˜framework’.

Stop for a second: It’s all about your workflow

We deal with tooling and features, but it’s actually meaningless evaluating these options before determining whether your preferred workflow is synchronized or independent (we will discuss this in a few seconds). This upfront fundamental decision will change almost everything.

Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?

Option A β€” The synchronized workflow- Going with this development style, all the three components will be developed and deployed in one chunk together. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed together.

Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk’s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.

Option B β€” Independent workflow- This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change β€” The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week β€” deploy it.

Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components β€” some are coded by different teams. This workflow also forces her to write efficient tests against the library β€” it’s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client’s perspective loses some dimension of realism. Also, if a single developer has to update 5 units β€” publishing each individually to the registry and then updating within all the dependencies can be a little tedious.

Synchronized and independent workflows illustrated

On the illusion of synchronicity

In distributed systems, it’s not feasible to achieve 100% synchronicity β€” believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2’s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow β€” The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.

This fundamental decision, synchronized or independent, will determine so many things β€” Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package’s folder, and whether to create a local link between packages which is described in the next paragraph.

Layer 4: Link your packages for immediate feedback

When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:

Option 1: Using npm β€” Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.

Option2: Just a plain folder β€” With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it’s just code in a dedicated folder. This is for example how Nest.js modules are represented.

With option 1, teams benefit from all the great merits of a package manager β€” SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won’t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers β€” one can’t just code over multiple packages and test/run the changes.

With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.

How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time β€” the copy is grabbed from the registry.

Linking packages in a Monorepo

If you’re doing the synchronized workflow, you’re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.

If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a β€˜monolith monorepo’, or maybe a β€˜monolitho’. However, when not linking, it’s harder to debug a small issue between the Microservice and the npm library. What I typically do is temporarily link (with npm link) between the packages, debug, code, then finally remove the link.

Nx is taking a slightly more disruptive approach β€” it is using TypeScript paths to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.

Closing: What should you use?

It’s all about your workflow and architecture β€” a huge unseen cross-road stands in front of the Monorepo tooling decision.

Scenario A β€” If your architecture dictates a synchronized workflow where all packages are deployed together, or at least developed in collaboration β€” then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.

For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you’re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly β€” for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many relatively coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.

If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.

Scenario Bβ€” If you’re into an independent workflow where each package is developed, tested, and deployed (almost) independently β€” then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool β€” Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is Bilt by Gil Tayar, it’s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.

In any scenario, consider workspaces β€” If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you’re working in an autonomous workflow, smaller are the chances of facing such issues. Don’t just use tools unless there is a pain.

We tried to show the beauty of each and where it shines. If we’re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.

Bonus: Comparison table

See below a detailed comparison table of the various tools and features:

Preview only, the complete table can be found here

]]>
monorepo diff --git a/blog/tags/component-test/index.html b/blog/tags/component-test/index.html index 07329922..305a4f35 100644 --- a/blog/tags/component-test/index.html +++ b/blog/tags/component-test/index.html @@ -9,13 +9,13 @@ - +
-

One post tagged with "component-test"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

- +

One post tagged with "component-test"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

+ \ No newline at end of file diff --git a/blog/tags/decisions/index.html b/blog/tags/decisions/index.html index 68c218ae..25f7aa13 100644 --- a/blog/tags/decisions/index.html +++ b/blog/tags/decisions/index.html @@ -9,13 +9,13 @@ - +
-

One post tagged with "decisions"

View All Tags

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

- +

One post tagged with "decisions"

View All Tags

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

+ \ No newline at end of file diff --git a/blog/tags/dotenv/index.html b/blog/tags/dotenv/index.html index bfb6f4b3..c76c91c3 100644 --- a/blog/tags/dotenv/index.html +++ b/blog/tags/dotenv/index.html @@ -9,13 +9,13 @@ - +

2 posts tagged with "dotenv"

View All Tags

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
- + \ No newline at end of file diff --git a/blog/tags/express/index.html b/blog/tags/express/index.html index 1ee3b076..ccc0393c 100644 --- a/blog/tags/express/index.html +++ b/blog/tags/express/index.html @@ -9,13 +9,13 @@ - +

4 posts tagged with "express"

View All Tags

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

- + \ No newline at end of file diff --git a/blog/tags/fastify/index.html b/blog/tags/fastify/index.html index 007474df..35b73694 100644 --- a/blog/tags/fastify/index.html +++ b/blog/tags/fastify/index.html @@ -9,13 +9,13 @@ - +
-

4 posts tagged with "fastify"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

- +

4 posts tagged with "fastify"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

+ \ No newline at end of file diff --git a/blog/tags/index.html b/blog/tags/index.html index b64659d4..502275c6 100644 --- a/blog/tags/index.html +++ b/blog/tags/index.html @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git a/blog/tags/integration/index.html b/blog/tags/integration/index.html index 1dae09a9..4dfc7887 100644 --- a/blog/tags/integration/index.html +++ b/blog/tags/integration/index.html @@ -9,13 +9,13 @@ - +
-

One post tagged with "integration"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

- +

One post tagged with "integration"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

+ \ No newline at end of file diff --git a/blog/tags/monorepo/index.html b/blog/tags/monorepo/index.html index d4c6a35b..828b3cb5 100644 --- a/blog/tags/monorepo/index.html +++ b/blog/tags/monorepo/index.html @@ -9,13 +9,13 @@ - +
-

One post tagged with "monorepo"

View All Tags

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

- +

One post tagged with "monorepo"

View All Tags

Β· 17 min read
Yoni Goldberg
Michael Salomon

As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling

Monorepos

What are we lookingΒ at​

The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries β€” Lerna- has just retired. When looking closely, it might not be just a coincidence β€” With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused β€” What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you’re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.

This post is concerned with backend-only and Node.js. It also scoped to typical business solutions. If you’re Google/FB developer who is faced with 8,000 packages β€” sorry, you need special gear. Consequently, monster Monorepo tooling like Bazel is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it’s not actually maintained anymore β€” it’s a good baseline for comparison).

Let’s start? When human beings use the term Monorepo, they typically refer to one or more of the following 4 layers below. Each one of them can bring value to your project, each has different consequences, tooling, and features:

+ \ No newline at end of file diff --git a/blog/tags/nestjs/index.html b/blog/tags/nestjs/index.html index f8364803..025841a8 100644 --- a/blog/tags/nestjs/index.html +++ b/blog/tags/nestjs/index.html @@ -9,13 +9,13 @@ - +

2 posts tagged with "nestjs"

View All Tags

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
- + \ No newline at end of file diff --git a/blog/tags/nock/index.html b/blog/tags/nock/index.html index 4ae5467f..5e424002 100644 --- a/blog/tags/nock/index.html +++ b/blog/tags/nock/index.html @@ -9,13 +9,13 @@ - +
-

One post tagged with "nock"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

- +

One post tagged with "nock"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

+ \ No newline at end of file diff --git a/blog/tags/node-js/index.html b/blog/tags/node-js/index.html index de4f7244..ffcfe5a2 100644 --- a/blog/tags/node-js/index.html +++ b/blog/tags/node-js/index.html @@ -9,13 +9,13 @@ - +
-

5 posts tagged with "node.js"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

- +

5 posts tagged with "node.js"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV

Β· 2 min read
Yoni Goldberg

πŸ₯³ We're thrilled to launch the very first version of Practica.js.

What is Practica is one paragraph​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices.

Your developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you'll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app.

90 seconds video​

How to get started​

To get up to speed quickly, read our getting started guide.

+ \ No newline at end of file diff --git a/blog/tags/passport/index.html b/blog/tags/passport/index.html index 45ece240..11bad623 100644 --- a/blog/tags/passport/index.html +++ b/blog/tags/passport/index.html @@ -9,13 +9,13 @@ - +

2 posts tagged with "passport"

View All Tags

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
- + \ No newline at end of file diff --git a/blog/tags/practica/index.html b/blog/tags/practica/index.html index 88ba5a97..75cc7df5 100644 --- a/blog/tags/practica/index.html +++ b/blog/tags/practica/index.html @@ -9,13 +9,13 @@ - +

3 posts tagged with "practica"

View All Tags

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
- + \ No newline at end of file diff --git a/blog/tags/prisma/index.html b/blog/tags/prisma/index.html index f39a6218..1b047b08 100644 --- a/blog/tags/prisma/index.html +++ b/blog/tags/prisma/index.html @@ -9,13 +9,13 @@ - +

One post tagged with "prisma"

View All Tags

Β· 2 min read
Yoni Goldberg
Raz Luvaton
Daniel Gluskin
Michael Salomon

Where is our focus now?​

We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback

What's new?​

Request-level store​

Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in AsyncLocal for this task

Hardened .dockerfile​

Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from this article and already apply 90% of the guidelines

Additional ORM option: Prisma​

Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma

Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this blog post

Many small enhancements​

More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more

Where do I start?​

Definitely follow the getting started guide first and then read the guide coding with practica to realize its full power and genuine value. We will be thankful to receive your feedback

- + \ No newline at end of file diff --git a/blog/tags/supertest/index.html b/blog/tags/supertest/index.html index 07f54b09..18b405a7 100644 --- a/blog/tags/supertest/index.html +++ b/blog/tags/supertest/index.html @@ -9,13 +9,13 @@ - +

2 posts tagged with "supertest"

View All Tags

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
- + \ No newline at end of file diff --git a/blog/tags/testing/index.html b/blog/tags/testing/index.html index 673b64bf..59669e5e 100644 --- a/blog/tags/testing/index.html +++ b/blog/tags/testing/index.html @@ -9,13 +9,13 @@ - +
-

3 posts tagged with "testing"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
- +

3 posts tagged with "testing"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

Β· 24 min read
Yoni Goldberg

Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?​

Betteridge's law of headlines suggests that a 'headline that ends in a question mark can be answered by the word NO'. Will this article follow this rule?

Imagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it's hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained

Suite with stain

Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, "I wish we had something like (Java) hibernate or (.NET) Entity Framework" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don't feel delightful, some may say even mediocre. At least so I believed before writing this article...

From time to time, a shiny new ORM is launched, and there is hope. Then soon it's realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It's gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the 'Ferrari' ORM we've been waiting for? Is it a game changer? If you're are the 'no ORM for me' type, will this one make you convert your religion?

In Practica.js (the Node.js starter based off Node.js best practices with 83,000 stars) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?

This article is certainly not an 'ORM 101' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It's compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren't covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs

Ready to explore how good Prisma is and whether you should throw away your current tools?

Β· 22 min read
Yoni Goldberg

Node.js is maturing. Many patterns and frameworks were embraced - it's my belief that developers' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?

In his novel book 'Atomic Habits' the author James Clear states that:

"Mastery is created by habits. However, sometimes when we're on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot". In other words, practice makes perfect, and bad practices make things worst

We copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change

Luckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples.

Are those disruptive thoughts surely correct? I'm not sure. There is one things I'm sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not "don't use this tool!" but rather becoming familiar with other techniques that, under some circumstances might be a better fit

Animals and frameworks shed their skin

The True Crab's exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell

TOC - Patterns to reconsider​

  1. Dotenv
  2. Calling a service from a controller
  3. Nest.js dependency injection for all classes
  4. Passport.js
  5. Supertest
  6. Fastify utility decoration
  7. Logging from a catch clause
  8. Morgan logger
  9. NODE_ENV
+ \ No newline at end of file diff --git a/blog/tags/unit-test/index.html b/blog/tags/unit-test/index.html index aa990bc2..084a3bf1 100644 --- a/blog/tags/unit-test/index.html +++ b/blog/tags/unit-test/index.html @@ -9,13 +9,13 @@ - +
-

One post tagged with "unit-test"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

- +

One post tagged with "unit-test"

View All Tags

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

+ \ No newline at end of file diff --git a/blog/testing-the-dark-scenarios-of-your-nodejs-application/index.html b/blog/testing-the-dark-scenarios-of-your-nodejs-application/index.html index 4f930fa9..3d8a8b4d 100644 --- a/blog/testing-the-dark-scenarios-of-your-nodejs-application/index.html +++ b/blog/testing-the-dark-scenarios-of-your-nodejs-application/index.html @@ -9,13 +9,13 @@ - +
-

Testing the dark scenarios of your Node.js application

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because dead bodies are covered in this phase. First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

- +

Testing the dark scenarios of your Node.js application

Β· 20 min read
Yoni Goldberg
Raz Luvaton

Where the dead-bodies are covered​

This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked

Some context first: How do we test a modern backend? With the testing diamond, of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written a guide with 50 best practices for integration tests in Node.js

But there is a pitfall: most developers write only semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime

The hidden corners

Here are a handful of examples that might open your mind to a whole new class of risks and tests

πŸ§Ÿβ€β™€οΈ The zombie process test​

πŸ‘‰What & so what? - In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see readiness probe). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!

πŸ“ Code

Code under test, api.js:

// A common express server initialization
const startWebServer = () => {
return new Promise((resolve, reject) => {
try {
// A typical Express setup
expressApp = express();
defineRoutes(expressApp); // a function that defines all routes
expressApp.listen(process.env.WEB_SERVER_PORT);
} catch (error) {
//log here, fire a metric, maybe even retry and finally:
process.exit();
}
});
};

The test:

const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function
const sinon = require('sinon'); // a mocking library

test('When an error happens during the startup phase, then the process exits', async () => {
// Arrange
const processExitListener = sinon.stub(process, 'exit');
// πŸ‘‡ Choose a function that is part of the initialization phase and make it fail
sinon
.stub(routes, 'defineRoutes')
.throws(new Error('Cant initialize connection'));

// Act
await api.startWebServer();

// Assert
expect(processExitListener.called).toBe(true);
});

πŸ‘€ The observability test​

πŸ‘‰What & why - For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error correctly observable. In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, including stack trace, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:

πŸ“ Code

test('When exception is throw during request, Then logger reports the mandatory fields', async () => {
//Arrange
const orderToAdd = {
userId: 1,
productId: 2,
status: 'approved',
};
const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');
sinon
.stub(OrderRepository.prototype, 'addOrder')
.rejects(new AppError('saving-failed', 'Order could not be saved', 500));
const loggerDouble = sinon.stub(logger, 'error');

//Act
await axiosAPIClient.post('/order', orderToAdd);

//Assert
expect(loggerDouble).toHaveBeenCalledWith({
name: 'saving-failed',
status: 500,
stack: expect.any(String),
message: expect.any(String),
});
expect(
metricsExporterDouble).toHaveBeenCalledWith('error', {
errorName: 'example-error',
})
});

πŸ‘½ The 'unexpected visitor' test - when an uncaught exception meets our code​

πŸ‘‰What & why - A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, hopefully if your code subscribed. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:

researches says that, rejection

πŸ“ Code

test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {
//Arrange
const loggerDouble = sinon.stub(logger, 'error');
const processExitListener = sinon.stub(process, 'exit');
const errorToThrow = new Error('An error that wont be caught 😳');

//Act
process.emit('uncaughtException', errorToThrow); //πŸ‘ˆ Where the magic is

// Assert
expect(processExitListener.called).toBe(false);
expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);
});

πŸ•΅πŸΌ The 'hidden effect' test - when the code should not mutate at all​

πŸ‘‰What & so what - In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:

πŸ“ Code

it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {
//Arrange
const orderToAdd = {
userId: 1,
mode: 'draft',
externalIdentifier: uuid(), //no existing record has this value
};

//Act
const { status: addingHTTPStatus } = await axiosAPIClient.post(
'/order',
orderToAdd
);

//Assert
const { status: fetchingHTTPStatus } = await axiosAPIClient.get(
`/order/externalIdentifier/${orderToAdd.externalIdentifier}`
); // Trying to get the order that should have failed
expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({
addingHTTPStatus: 400,
fetchingHTTPStatus: 404,
});
// πŸ‘† Check that no such record exists
});

🧨 The 'overdoing' test - when the code should mutate but it's doing too much​

πŸ‘‰What & why - This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:

πŸ“ Code

test('When deleting an existing order, Then it should NOT be retrievable', async () => {
// Arrange
const orderToDelete = {
userId: 1,
productId: 2,
};
const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data
.id; // We will delete this soon
const orderNotToBeDeleted = orderToDelete;
const notDeletedOrder = (
await axiosAPIClient.post('/order', orderNotToBeDeleted)
).data.id; // We will not delete this

// Act
await axiosAPIClient.delete(`/order/${deletedOrder}`);

// Assert
const { status: getDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${deletedOrder}`
);
const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(
`/order/${notDeletedOrder}`
);
expect(getNotDeletedOrderStatus).toBe(200);
expect(getDeletedOrderStatus).toBe(404);
});

πŸ•° The 'slow collaborator' test - when the other HTTP service times out​

πŸ‘‰What & why - When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like nock or wiremock. These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available in production, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use fake timers and trick the system into believing as few seconds passed in a single tick. If you're using nock, it offers an interesting feature to simulate timeouts quickly: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting

πŸ“ Code

// In this example, our code accepts new Orders and while processing them approaches the Users Microservice
test('When users service times out, then return 503 (option 1 with fake timers)', async () => {
//Arrange
const clock = sinon.useFakeTimers();
config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls
nock(`${config.userServiceURL}/user/`)
.get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout πŸ‘†
.reply(200);
const loggerDouble = sinon.stub(logger, 'error');
const orderToAdd = {
userId: 1,
productId: 2,
mode: 'approved',
};

//Act
// πŸ‘‡try to add new order which should fail due to User service not available
const response = await axiosAPIClient.post('/order', orderToAdd);

//Assert
// πŸ‘‡At least our code does its best given this situation
expect(response.status).toBe(503);
expect(loggerDouble.lastCall.firstArg).toMatchObject({
name: 'user-service-not-available',
stack: expect.any(String),
message: expect.any(String),
});
});

πŸ’Š The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation​

πŸ‘‰What & so what - When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why

When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. SQS demand 60 seconds to purge queues), to name a few challenges that you won't find when dealing with real DB

Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like this one for SQS and you can code one easily yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):

πŸ“ Code

  1. Create a fake message queue that does almost nothing but record calls, see full example here
class FakeMessageQueueProvider extends EventEmitter {
// Implement here

publish(message) {}

consume(queueName, callback) {}
}
  1. Make your message queue client accept real or fake provider
class MessageQueueClient extends EventEmitter {
// Pass to it a fake or real message queue
constructor(customMessageQueueProvider) {}

publish(message) {}

consume(queueName, callback) {}

// Simple implementation can be found here:
// https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js
}
  1. Expose a convenient function that tells when certain calls where made
class MessageQueueClient extends EventEmitter {
publish(message) {}

consume(queueName, callback) {}

// πŸ‘‡
waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise
}
  1. The test is now short, flat and expressive πŸ‘‡
const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');
const MessageQueueClient = require('./libs/message-queue-client');
const newOrderService = require('./domain/newOrderService');

test('When a poisoned message arrives, then it is being rejected back', async () => {
// Arrange
const messageWithInvalidSchema = { nonExistingProperty: 'invalid❌' };
const messageQueueClient = new MessageQueueClient(
new FakeMessageQueueProvider()
);
// Subscribe to new messages and passing the handler function
messageQueueClient.consume('orders.new', newOrderService.addOrder);

// Act
await messageQueueClient.publish('orders.new', messageWithInvalidSchema);
// Now all the layers of the app will get stretched πŸ‘†, including logic and message queue libraries

// Assert
await messageQueueClient.waitFor('reject', { howManyTimes: 1 });
// πŸ‘† This tells us that eventually our code asked the message queue client to reject this poisoned message
});

πŸ“Full code example - is here

πŸ“¦ Test the package as a consumer​

πŸ‘‰What & why - When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts that were built. See the mismatch here? after running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files

πŸ“ Code

Consider the following scenario, you're developing a library, and you wrote this code:

// index.js
export * from './calculate.js';

// calculate.js πŸ‘ˆ
export function calculate() {
return 1;
}

Then some tests:

import { calculate } from './index.js';

test('should return 1', () => {
expect(calculate()).toBe(1);
})

βœ… All tests pass 🎊

Finally configure the package.json:

{
// ....
"files": [
"index.js"
]
}

See, 100% coverage, all tests pass locally and in the CI βœ…, it just won't work in production πŸ‘Ή. Why? because you forgot to include the calculate.js in the package.json files array πŸ‘†

What can we do instead? we can test the library as its end-users. How? publish the package to a local registry like verdaccio, let the tests install and approach the published code. Sounds troublesome? judge yourself πŸ‘‡

πŸ“ Code

// global-setup.js

// 1. Setup the in-memory NPM registry, one function that's it! πŸ”₯
await setupVerdaccio();

// 2. Building our package
await exec('npm', ['run', 'build'], {
cwd: packagePath,
});

// 3. Publish it to the in-memory registry
await exec('npm', ['publish', '--registry=http://localhost:4873'], {
cwd: packagePath,
});

// 4. Installing it in the consumer directory
await exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {
cwd: consumerPath,
});

// Test file in the consumerPath

// 5. Test the package πŸš€
test("should succeed", async () => {
const { fn1 } = await import('my-package');

expect(fn1()).toEqual(1);
});

πŸ“Full code example - is here

What else this technique can be useful for?

  • Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that
  • You want to test ESM and CJS consumers
  • If you have CLI application you can test it like your users
  • Making sure all the voodoo magic in that babel file is working as expected

πŸ—ž The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug​

πŸ‘‰What & so what - Quite confidently I'm sure that almost no team test their OpenAPI correctness. "It's just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.

Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., PACT), there are also leaner approaches that gets you covered easily and quickly (at the price of covering less risks).

The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:

πŸ“ Code

Code under test, an API throw a new error status

if (doesOrderCouponAlreadyExist) {
throw new AppError('duplicated-coupon', { httpStatus: 409 });
}

The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions

"responses": {
"200": {
"description": "successful",
}
,
"400": {
"description": "Invalid ID",
"content": {}
},// No 409 in this listπŸ˜²πŸ‘ˆ
}

The test code

const jestOpenAPI = require('jest-openapi');
jestOpenAPI('../openapi.json');

test('When an order with duplicated coupon is added , then 409 error should get returned', async () => {
// Arrange
const orderToAdd = {
userId: 1,
productId: 2,
couponId: uuid(),
};
await axiosAPIClient.post('/order', orderToAdd);

// Act
// We're adding the same coupon twice πŸ‘‡
const receivedResponse = await axios.post('/order', orderToAdd);

// Assert;
expect(receivedResponse.status).toBe(409);
expect(res).toSatisfyApiSpec();
// This πŸ‘† will throw if the API response, body or status, is different that was it stated in the OpenAPI
});

Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches

beforeAll(() => {
axios.interceptors.response.use((response) => {
expect(response.toSatisfyApiSpec());
// With this πŸ‘†, add nothing to the tests - each will fail if the response deviates from the docs
});
});

Even more ideas​

  • Test readiness and health routes
  • Test message queue connection failures
  • Test JWT and JWKS failures
  • Test security-related things like CSRF tokens
  • Test your HTTP client retry mechanism (very easy with nock)
  • Test that the DB migration succeed and the new code can work with old records format
  • Test DB connection disconnects

It's not just ideas, it a whole new mindset​

The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'

+ \ No newline at end of file diff --git a/contribution/contribution-long-guide/index.html b/contribution/contribution-long-guide/index.html index 550a8fa1..2004331f 100644 --- a/contribution/contribution-long-guide/index.html +++ b/contribution/contribution-long-guide/index.html @@ -9,13 +9,13 @@ - +

The comprehensive contribution guide

You belong with us​

If you reached down to this page, you probably belong with us πŸ’œ. We are in an ever-going quest for better software practices. This journey can bring two things to your benefit: A lot of learning and global impact on many people's craft. Does this sounds attractive?

Consider the shortened guide first​


Every small change can make this repo much better. If you intend to contribute a relatively small change like documentation change, small code enhancement or anything that is small and obvious - start by reading the shortened guide here. As you'll expand your engagement with this repo, it might be a good idea to visit this long guide again

Philosophy​

Our main selling point is our philosophy, our philosophy is 'make it SIMPLE'. There is one really important holy grail in software - Speed. The faster you move, the more features and value is created for the users. The faster you move, more improvements cycles are deployed and the software/ops become better. Researches show that faster team produces software that is more reliable. Complexity is the enemy of speed - Commonly apps are big, sophisticated, has a lot of internal abstractions and demand long training before being productive. Our mission is to minimize complexity, get onboarded developers up to speed quickly, or in simple words - Let the reader of the code understand it in a breeze. If you make simplicity a 1st principle - Great things will come your way.

The sweet spot

Big words, how exactly? Here are few examples:

- Simple language - We use TypeScript because we believe in types, but we minimize advanced features. This boils down to using functions only, sometimes also classes. No abstracts, generic, complex types or anything that demand more CPU cycles from the reader.

- Less generic - Yes, you read it right. If you can code a function that covers less scenarios but is shorter and simpler to understand - Consider this option first. Sometimes one if forced to make things generic - That's fine, at least we minimized the amount of complex code locations

- Simple tools - Need to use some 3rd party for some task? Choose the library that is doing the minimal amount of work. For example, when seeking a library that parses JWT tokens - avoid picking a super-fancy framework that can solve any authorization path (e.g., Passport). Instead, Opt for a library that is doing exactly this. This will result in code that is simpler to understand and reduced bug surface

- Prefer Node/JavaScript built-in tooling - Some new frameworks have abstractions over some standard tooling. They have their way of defining modules, libraries and others which demand learning one more concept and being exposed to unnecessary layer of bugs. Our preferred way is the vanilla way, if it's part of JavaScript/Node - We use it. For example, should we need to group a bunch of files as a logical modules - We use ESM to export the relevant files and functions

Our full coding guide will come here soon

Workflow​

Got a small change? Choose the fast lane​

Every small change can make this repo much better. If you intend to contribute a relatively small change like documentation change, linting rules, look&feel fixes, fixing TYPOs, comments or anything that is small and obvious - Just fork to your machine, code, ensure all tests pass (e.g., npm test), PR with a meaningful title, get 1 approver before merging. That's it.

Need to change the code itself? Here is a typical workflow​

➑️ Idea➑ Design decisions➑ Code➑️ Merge
WhenGot an idea how to improve? Want to handle an existing issue?When the change implies some major decisions, those should be discussed in advanceWhen got confirmation from core maintainer that the design decisions are sensibleWhen you have accomplished a short iteration . If the whole change is small, PR in the end
What1. Create an issue (if doesn't exist)
2. Label the issue with the its type (e.g., question, bug) and the area of improvement (e.g., area-generator, area-express)
3. Comment and specify your intent to handle this issue
1. Within the issue, specify your overall approach/design. Or just open a discussion 2. If choosing a 3rd party library, ensure to follow our standard decision and comparison template. Example can be found here1. Do it with passions πŸ’œ
2. Follow our coding guide. Keep it simple. Stay loyal to our philosophy
3. Run all the quality measures frequently (testing, linting)
1. Share your progress early by submit a work in progress PR
2. Ensure all CI checks pass (e.g., testing)
3. Get at least one approval before merging

Roles​

Project structure​

High-level sections​

The repo has 3 root folders that represents what we do:

  • docs - Anything we write to make this project super easy to work with
  • code-generator - A tool with great DX to choose and generate the right app for the user
  • code-templates - The code that we generate with the right patterns and practices
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#99BF2C','secondaryColor':'#C2DF84','lineColor':'#ABCA64','fontWeight': 'bold', 'fontFamily': 'comfortaa, Roboto'}}}%%
graph
A[Practica] -->|How we create apps| B(Code Generators)
A -->|The code that we generate!| C(Code Templates)
A -->|How we explain ourself| D(Docs)


The code templates​

Typically, the two main sections are the Microservice (apps) and cross-cutting-concern libraries:

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#99BF2C','secondaryColor':'#C2DF84','lineColor':'#ABCA64','fontWeight': 'bold', 'fontFamily': 'comfortaa, Roboto'}}}%%
graph
A[Code Templates] -->|The example Microservice/app| B(Services)
B -->|Where the API, logic and data lives| D(Example Microservice)
A -->|Cross Microservice concerns| C(Libraries)
C -->|Explained in a dedicated section| K(*Multiple libraries like logger)
style D stroke:#333,stroke-width:4px


The Microservice structure

The entry-point of the generated code is an example Microservice that exposes API and has the traditional layers of a component:

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#99BF2C','secondaryColor':'#C2DF84','lineColor':'#ABCA64','fontWeight': 'bold', 'fontFamily': 'comfortaa, Roboto'}}}%%
graph
A[Services] -->|Where the API, logic and data lives| D(Example Microservice)
A -->|Almost empty, used to exemplify<br/> Microservice communication| E(Collaborator Microservice)
D -->|The web layer with REST/Graph| G(Web/API layer)
N -->|Docker-compose based DB, MQ and Cache| F(Infrastructure)
D -->|Where the business lives| M(Domain layer)
D -->|Anything related with database| N(Data-access layer)
D -->|Component-wide testing| S(Testing)
style D stroke:#333,stroke-width:4px

Libraries

All libraries are independent npm packages that can be testing in isolation

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#99BF2C','secondaryColor':'#C2DF84','lineColor':'#ABCA64','fontWeight': 'bold', 'fontFamily': 'comfortaa, Roboto'}}}%%
graph
A[Libraries] --> B(Logger)
A[Libraries] --> |Token-based auth| C(Authorization)
A[Libraries] --> |Retrieve and validate the configuration| D(Configuration)
A[Libraries] --> E(Error handler)
A[Libraries] --> E(MetricsService)
A[Libraries] --> Z(More to come...)
style Z stroke:#333,stroke-width:4px

The code generator structure​

Packages (domains)​

This solution is built around independent domains that share almost nothing with others. It is recommended to start with understanding a single and small domain (package), then expanding and getting acquainted with more. This is also an opportunity to master a specific topic that you're passionate about. Following is our packages list, choose where you wish to contribute first

PackageWhatStatusChosen libsQuick links
microservice/expressA web layer of an example Microservice based on expressjsπŸ§“πŸ½ Stable-- Code & readme
- Issues & ideas
microservice/fastifyA web layer of an example Microservice based on Fastify🐣 Not started

(Take the heel, open an issue)
-- Code & readme
- Issues & ideas
microservice/dal/prismaA DAL layer of an example Microservice based on Prisma.jsπŸ₯ Beta/skeleton-- Code & readme
- Issues & ideas
library/loggerA logging library wrapperπŸ₯ Beta/skeleton

(Take it!)
Pino

Why: Decision here
- Code & readme
- Issues & ideas
library/configurationA library that validates, reads and serve configurationπŸ§’πŸ» Solid

(Improvements needed)
Convict

Why: Decision here
- Code & readme
- Issues & ideas
library/jwt-based-authenticationA library that authenticates requests with JWT tokenπŸ§“πŸ½ Stablejsonwebtoken

Why:
Decision here
- Code & readme
- Issues & ideas

Development machine setup​

βœ… Ensure Node, Docker and NVM are installed

βœ… Configure GitHub and npm 2FA!

βœ… Close the repo if you are a maintainer, or fork it if have no collaborators permissions

βœ… With your terminal, ensure the right Node version is installed:

nvm use

βœ… Install dependencies:

nvm i

βœ… Ensure all tests pass:

npm t

βœ… Code. Run the test. And vice versa

Areas to focus on​

domains

Supported Node.js version​

  • The generated code should be compatible with Node.js versions >14.0.0.
  • It's fair to demand LTS version from the repository maintainers (the generator code)

Code structure​

Soon

- + \ No newline at end of file diff --git a/contribution/contribution-short-guide/index.html b/contribution/contribution-short-guide/index.html index 978ce844..dc445a91 100644 --- a/contribution/contribution-short-guide/index.html +++ b/contribution/contribution-short-guide/index.html @@ -9,13 +9,13 @@ - +

Contributing to Practica.js - The short guide

You belong with us​

We are in an ever-going quest for better software practices. If you reached down to this page, you probably belong with us πŸ’œ.

Note: This is a shortened guide that suits those are willing to quickly contribute. Once you deepen your relations with Practica.js - It's a good idea to read the full guide

2 things to consider​

  • Our philosophy is all about minimalism and simplicity - We strive to write less code, rely on existing and reputable libraries, stick to Node/JS standards and avoid adding our own abstractions
  • Popular vendors only - Each technology and vendor that we introduce must super popular and reliable. For example, a library must one of the top 5 most starred and downloaded in its category. . See full vendor choose instructions here

The main internals tiers (in a nutshell)​

For a quick start, you don't necessarily need to understand the entire codebase. Typically, your contribution will fall under one of these three categories:

Option 1 - External or configuration change​

High-level changes

If you simply mean to edit things beyond the code - There is no need to delve into the internals. For example, when changing documentation, CI/bots, and alike - One can simply perform the task without delving into the code

Option 2 - The code generator​

Code and CLI to get the user preferences and copy the right code to her computer

Here you will find CLI, UI, and logic to generate the right code. We run our own custom code to go through the code-template folder and filter out parts/files based on the user preferences. For example, should she ask NOT to get a GitHub Actions file - The generator will remove this file from the output

How to work with it?

  1. If all you need is to alter the logic, you may just code in the ~/code-generator/generation-logic folder and run the tests (located in the same folder)
  2. If you wish to modify the CLI UI, then you'll need to build the code before running (because there is no way to run TypeScript in CLI). Open two terminals:
  • Open one terminal to compile the code:
npm run build:watch
  • Open second terminal to run the CLI UI:
npm run start:cli

Option 3 - The code templates​

The output of our program: An example Microservice and libraries

Here you will the generated code that we will selectively copy to the user's computer which is located under {root}/src/code-templates. It's preferable to work on this code outside the main repository in some side folder. To achieve this, simply generate the code using the CLI, code, run the tests, then finally copy to the main repository

  1. Install dependencies
nvm use && npm i
  1. Build the code
npm run build
  1. Bind the CLI command to our code
cd .dist && npm link
  1. Generate the code to your preferred working folder
cd {some folder like $HOME}
create-node-app immediate --install-dependencies
  1. Now you can work on the generated code. Later on, once your tests pass and you're happy - copy the changes back to ~/practica/src/code-templates

  2. Run the tests while you code

#From the folder where you generated the code to. You might need to 'git init'
cd default-app-name/services/order-service
npm run test:dev

Workflow​

  1. Idea - Claim an existing issue or open a new one
  2. Optional: Design - If you're doing something that is not straightforward, share your high-level approach to this within the issue
  3. PR - Once you're done, run the tests locally then PR to main. Ensure all checks pass. If you introduced a new feature - Update the docs

Development machine setup​

βœ… Ensure Node, Docker and NVM are installed

βœ… Configure GitHub and npm 2FA!

βœ… Close the repo if you are a maintainer, or fork it if have no collaborators permissions

βœ… With your terminal, ensure the right Node version is installed:

nvm use

βœ… Install dependencies:

npm i

βœ… Ensure all tests pass:

npm t

βœ… You can safely start now: Code, run the test and vice versa

- + \ No newline at end of file diff --git a/contribution/release-checklist/index.html b/contribution/release-checklist/index.html index 0a8ffe9b..c47490a9 100644 --- a/contribution/release-checklist/index.html +++ b/contribution/release-checklist/index.html @@ -9,13 +9,13 @@ - +

A checklist for releasing a new Practica version

βœ… Bump package.json of both root and example Microservice

βœ… Ensure you're on the master branch

βœ… Publish from the root

npm run publish:build

βœ… Test manually by cleaning local .bin and running the get started guide

- + \ No newline at end of file diff --git a/contribution/vendor-pick-guidelines/index.html b/contribution/vendor-pick-guidelines/index.html index e8a35411..021fe8bc 100644 --- a/contribution/vendor-pick-guidelines/index.html +++ b/contribution/vendor-pick-guidelines/index.html @@ -9,13 +9,13 @@ - +

Choosing npm package dependency thoughtfully

βœ… The decision must follow a comparison table of options using this template

βœ… Usage state must be captured including weekly downloads, GitHub stars and dependents. Only top 5 most popular vendors can be evaluated

βœ… The evaluated libs must have been updated at least once in the last 6 months

- + \ No newline at end of file diff --git a/decisions/configuration-library/index.html b/decisions/configuration-library/index.html index 80f4f8cb..27126c4b 100644 --- a/decisions/configuration-library/index.html +++ b/decisions/configuration-library/index.html @@ -9,13 +9,13 @@ - +

Decision: Choosing a configuration library

πŸ“” What is it - A decision data and discussion about the right configuration library

⏰ Status - Open, closed in April 1st 2022

πŸ“ Corresponding discussion - Here

🎯Bottom-line: our recommendation - ✨convict✨ ticks all the boxes by providing both strict schema, fail fast option, entry documentation and hierarchical structure

πŸ“Š Detailed comparison table

dotenvConvictnconfconfig
Executive Summary
Performance (load time for 100 keys)Full
1ms
Almost full
5ms
Almost full
4ms
Almost full
5ms
PopularityFull
Superior
Partial
Less popular than competitors
Almost full
Highly popular
Almost full
Highly popular
❗ Fail fast & strict schemaAlmost full
No
Full
Yes
Partial
No
Partial
No
Items documentationPartial
No
Full
Yes
Partial
No
Partial
No
Hierarchical configuration schemaPartial
No
Full
Yes
Full
Yes
Partial
No
More details: Community & Popularity - March 2022
Stars4200 ✨2500 ✨2500 ✨1000 ✨
Downloads/Week12,900,223 πŸ“4,000,000 πŸ“6,000,000 πŸ“5,000,000 πŸ“
Dependents26,000 πŸ‘©β€πŸ‘§600 πŸ‘§800 πŸ‘§1000 πŸ‘§
- + \ No newline at end of file diff --git a/decisions/docker-base-image/index.html b/decisions/docker-base-image/index.html index 4d8e290a..c9bb91f0 100644 --- a/decisions/docker-base-image/index.html +++ b/decisions/docker-base-image/index.html @@ -9,13 +9,13 @@ - +

Decision: Choosing a Docker base image

πŸ“” What is it - The Dockerfile that is included inherits from a base Node.js image. There are variois considerations when choosing the right option which are listed below

⏰ Status - Open for discussions

πŸ“ Corresponding discussion - Here

🎯Bottom-line: our recommendation - TBD

πŸ“Š Detailed comparison table

full-blown

bullseye-slim

alpine

Key Dimensions

Officially supported

Yes


Yes


No? Looking for sources
CVEs (Medium severity and above)

❗️Trivy: 521, Snyk: TBD


Trivy: 11 high, Snyk: TBD


Trivy: 0 high, Snyk: TBD
Size in MB

950 MB


150 MB


90 MB
Native modules installation
Packages that run native code installer (e.g., with node-gyp)


Standard C compiler glibc


Standard C compiler glibc


A less standard compiler, musl - might break under some circumstances

Other important dimensions to consider?

- + \ No newline at end of file diff --git a/decisions/index.html b/decisions/index.html index 72f31cf9..dd77bb4e 100644 --- a/decisions/index.html +++ b/decisions/index.html @@ -9,13 +9,13 @@ - +

Decision making documentation

Making our decisions transparent and collaborative is at the heart of Practica. In this folder, all decisions should be documented using our decision template

Index​

- + \ No newline at end of file diff --git a/decisions/monorepo/index.html b/decisions/monorepo/index.html index cd5a5e2b..db0033df 100644 --- a/decisions/monorepo/index.html +++ b/decisions/monorepo/index.html @@ -9,13 +9,13 @@ - +

Decision: Choosing Monorepo approach and tooling

πŸ“” What is it - Choosing the right Monorepo tool and features for the boilerplate

⏰ Status - Open for discussions

πŸ“ Corresponding discussion - Here

🎯Bottom-line: our recommendation - TBD

πŸ“Š Detailed comparison table

*For some lacking features there is a community package that bridges the gap; For workspace, we evaluated whether most of them support a specific feature****

nx

Turborepo

Lerna

workspace (npm, yarn, pnpm)

Executive Summary

Community and maintenance

Huge eco-system and commercial-grade maintenance


Trending, commercial-grade maintenance


Not maintained anymore


Solid
❗Encourage component autonomy

Packages are highly coupled


Workflow is coupled


npm link bypasses the SemVer


Minor concern: shared NODE_MODULES on the root
Build speed

Smart inference and execution plan, shared dependencies, cache


Smart inference and execution plan, shared dependencies, cache


Parallel tasks execution, copied dependencies


Shared dependencies
Standardization

Non standard Node.js stuff: One single root package.json by default, TS-paths for linking


An external build layer


An external build layer


An external package centralizer

Tasks and build pipeline

Run recursive commands (affect a group of packages)
Yes

Yes

Yes

Yes
❗️Parallel task execution
Yes

Yes

No

Yes* (Yarn & Pnpm)
❗️Realize which packages changed
Yes

Yes

Yes

No
❗️Realize packages that are affected by a change
Yes
both through package.json and code

Yes
through package.json

None

None
Ignore missing commands/scripts
No

Yes

Yes

Yes
❗️In-project cache - Skip tasks if local result exists
Yes

Yes

No

No
Remote cache - Skip tasks if remote result exists
Yes

Yes

No

No
Visual dependency graph
Yes

Yes

Partially, via plugin

No
❗️Smart waterfall pipeline - Schedule unrelated tasks in parallel, not topologically
Yes

Yes

No

No
Distributed task execution - Spread tasks across machines
Yes

No

No

No

Locally linking packages

❗️Is supportedPartially
Achieved through TS paths

No
Relies on workspaces

Yes

Yes
How
❗️Via TypeScript paths and webpack

Relies on workspaces

Symlink

Symlink
❗️Can opt-out?Yes
By default local packages are linked
-NoPartially
Pnpm allows preferring remote packages, Yarn has a [focused package](https://classic.yarnpkg.com/blog/2018/05/18/focused-workspaces/) option which only works per a single package
Link a range - only specific versions will be symlinkedNo-NoSome
Yarn and Pnpm allows workspace versioning

Optimizing dependencies installation speed

SupportedYes
Via a single Root package.json and NODE_MODULES
Yes
Via caching
No
Can be used on top of yarn workspace
Yes
Via single node_modules folder
Retain origin file path (some module refers to relative paths)Partially
NODE_MODULES is on the root, not per package
YesNot relevantPartially
Pnpm uses hard link instead of symlinks
Keep single NODE_MODULES per machine (faster, less disc space)No
NoNoPartially
Pnpm supports this

Other features and considerations

Community plugins
Yes

No

Yes

Yes
Scaffold new component from a gallery
Yes

None

None

None
Create a new package to the repo
Built it code genreation with useful templates

None, 3rd party code generator can be used

None, 3rd party code generator can be used

None, 3rd party code generator can be used
Adapt changes in the monorepo tool
Supported via nx migrate

Supported via codemod

None

None
Incremental builds
Supported

Supported

None

None
Cross-package modifications
Supported via nx generate

None

None

None

__

Ideas for next iteration:

  • Separate command execution and pipeline section
  • Stars and popularity
  • Features summary
  • Polyrepo support
- + \ No newline at end of file diff --git a/decisions/openapi/index.html b/decisions/openapi/index.html index 59c6c3c9..09923587 100644 --- a/decisions/openapi/index.html +++ b/decisions/openapi/index.html @@ -9,13 +9,13 @@ - +

Decision: Choosing _OpenAPI generator tooling

πŸ“” What is it - A decision data and discussion about the right OpenAPI tools and approach

⏰ Status - Open, closed in June 1st 2022

πŸ“ Corresponding discussion - Here

🎯Bottom-line: our recommendation - TBD

πŸ“Š Detailed comparison table

tsoa

JSON Schema

Other option 1

Other option 2

Executive Summary

Some dimension

1ms


5ms


4ms


5ms
Some dimension

Superior


Less popular than competitors


Highly popular


Highly popular
❗ Important factor

No


Yes


No


No

More details: Community & Popularity - March 2022

Stars
4200 ✨

2500 ✨

2500 ✨

1000 ✨
Downloads/Week
12,900,223 πŸ“

4,000,000 πŸ“

6,000,000 πŸ“

5,000,000 πŸ“
Dependents
26,000 πŸ‘©β€πŸ‘§

600 πŸ‘§

800 πŸ‘§

1000 πŸ‘§
- + \ No newline at end of file diff --git a/features/index.html b/features/index.html index 653627f5..089d41ad 100644 --- a/features/index.html +++ b/features/index.html @@ -9,13 +9,13 @@ - +

Coming soon: Features and practices

WIP - This doc is being written these days

This list will outline all the capabilities and roadmap of Practica.js

Here will come a filter panel to search by categories, what's strategic, and more

1. Logger​

1.1 Logger Library​

What: A reputable and hardened logger

Tags: #strategic #logger

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: Pino.js (Decision log here)

🎁 Bundles: example-flow, full-flow

🏁 CLI flags: --logger=true|false

1.2 Prevent infinite logger serialization loop​

What: Limit logged JSON depth when cyclic reference is introduced

Tags: #logger

πŸ‘·πŸΎ Status: Idea, not implemented

πŸ† Chosen libraries: Pino.js (Decision log here)

🎁 Bundles: example-flow, full-flow

🏁 CLI flags: None, always true

2. Configuration​

2.1 Configuration retriever module​

What: A configuration retriever module that packs good practices

Tags: #strategic #configuration

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: Convict (Decision log here)

🎁 Bundles: example-flow, full-flow

🏁 CLI flags: -

3. Testing experience​

3.1 Slow tests detection​

What: Slow tests automatically shown clearly in the console and exported to a json report

Tags: #dx #testing

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: jest-performance-reporter

🎁 Bundles: example-flow, full-flow

3.2 Autocomplete​

What: When running tests in watch mode and choosing filename or test name patterns autocomplete will assist you

Tags: #dx #testing

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: jest-watch-typeahead

4. Docker​

4.1 Secured dockerfile​

What: We build a production-ready .dockerfile that avoids leaking secrets and leaving dev dependencies in

Tags: #security #docker

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: N/A

4.1 Layered build​

What: The poduction artifact omit building tools to stay more compact and minimize attack sutface

Tags: #security #docker

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: N/A

4.2 Compact base image​

What: A small, ~100MB, base image of Node is used

Tags: #docker

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: N/A

4.2 Testing docker-compose​

What: Testing optimized database and other infrastrucuture running from docker-compose during the automated tests

Tags: #testing #docker #database

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: N/A

Additional 100 features will come here

5. Database​

5.1 Sequelize ORM​

What: Support for one of the most popular and matured ORM - Sequelize

Tags: #orm #db

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: Sequelize

5.2 Prisma ORM​

What: Support for one of an emerging and type safe ORM - Prisma

Tags: #orm #db

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: Prisma

5.3 Migration​

What: Includes migration files and commands for production-safe updates

Tags: #orm #db

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: Prisma

6. Request-level store​

6.1 Automatic correlation-id​

What: Automatically emit unique correlation id to every log line

Tags: #log #tracing

πŸ‘·πŸΎ Status: Production-ready, more hardening is welcome

πŸ† Chosen libraries: N/A

- + \ No newline at end of file diff --git a/index.html b/index.html index 74240fae..1d973b09 100644 --- a/index.html +++ b/index.html @@ -9,13 +9,13 @@ - +

home

Best practices starter


Generate a Node.js app that is packed with best practices AND simplicity in mind. Based off our repo Node.js best practices (77,000 stars)​


Discord Discord discussions | Twitter Twitter


A One Paragraph Overview

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices

1 min video πŸ‘‡

Our Philosophies and Unique Values

1. Best Practices on top of known Node.js frameworks​

We don't re-invent the wheel. Rather, we use your favorite framework and empower it with structure and real examples. With a single command you can get an Express/Fastify-based codebase with ~100 examples of best practices inside.

Built on top of known frameworks

2. Simplicity, how Node.js was intended​

Keeping it simple, flat and based on native Node/JS capabilities is part of this project DNA. We believe that too many abstractions, high-complexity or fancy language features can quickly become a stumbling block for the team.

To name a few examples, our code flow is flat with almost no level of indirection, although using TypeScript - almost no features are being used besides types, for modularization we simply use Node.js modules

Built on top of known frameworks

3. Supports many technologies and frameworks​

Good Practices and Simplicity is the name of the game with Practica. There is no need to narrow our code to a specific framework or database. We aim to support a majority of popular Node.js frameworks and databases.

Built on top of known frameworks


Practices and Features

We apply more than 100 practices and optimizations. You can opt in or out for most of these features using option flags on our CLI. The follow table is just a few examples of features we provide. To see the full list of features, please visit our website here.

FeatureExplanationFlagDocs
Monorepo setupGenerates two components (e.g., Microservices) in a single repository with interactions between the two--mr, --monorepoDocs here
Output escaping and sanitizingClean-out outgoing responses from potential HTML security risks like XSS--oe, --output-escapeDocs coming soon
Integration (component) testingGenerates full-blown component/integration tests setup including DB--t, --testsDocs coming soon
Unique request ID (Correlation ID)Generates module that creates a unique correlation/request ID for every incoming request. This is available for any other object during the request life-span. Internally it uses Node's built-in AsyncLocalStorage--coi, --correlation-idDocs coming soon
DockerfileGenerates dockerfile that embodies 20> best practices--df, --docker-fileDocs coming soon
Strong-schema configurationA configuration module that dynamically load run-time configuration keys and includes a strong schema so it can fail fastBuilt-in with basic appDocs here

πŸ“— See our full list of features here

- + \ No newline at end of file diff --git a/questions/index.html b/questions/index.html index 13dc1e6c..a0848e4e 100644 --- a/questions/index.html +++ b/questions/index.html @@ -9,13 +9,13 @@ - +

Common questions and answers

Testing your code​

Q: How to obtain a valid token to manually invoke the route (e.g., via POSTMAN)?​

Answer: By default, Practica routes are guarded from unauthorized requests. The automated testing already embed valid tokens. Should you wish to invoke the routes manually a token must be signed.

Option 1 - Visit an online JWT token signing tool like jwt builder, change the key (bottom part of the form) to the key that is specified under ./services/order-service/config.ts/jwtTokenSecret/default. If you never changed it, the default secret is: just-a-default-secret. Click the submit button and copy the generated token.

Given the signed token, add a new header to your request with the name 'Authorization' and the value 'Bearer {put here the token}'

Option 2 - We already generated this token for you πŸ‘‡, it should work with the default configuration in a development environment. Obviously, before going to production - the JWT secret must be changed:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjIwMTY5NjIsImV4cCI6MTY5MzU1Mjk2MiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.65ACAjHy2ZE5i_uS5hyiEkOQfkqOqdj-WtBm-w23qZQ

- + \ No newline at end of file diff --git a/the-basics/coding-with-practica/index.html b/the-basics/coding-with-practica/index.html index f1a06859..506aeb56 100644 --- a/the-basics/coding-with-practica/index.html +++ b/the-basics/coding-with-practica/index.html @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@

Coding with Practica

Now that you have Practice installed (if not, do this first), it's time to code a great app using it and understand its unique power. This journey will inspire you with good patterns and practices. All the concepts in this guide are not our unique ideas, quite the opposite, they are all standard patterns or libraries that we just put together. In this tutorial we will implement a simple feature using Practica, ready?

Pre-requisites​

Just before you start coding, ensure you have Docker and nvm (a utility that installs Node.js) installed. Both are common development tooling that are considered as a 'good practice'.

What's inside that box?​

You now have a folder with Practica code. What will you find inside this box? Practica created for you an example Node.js solution with a single component (API, Microservice) that is called 'order-service'. Of course you'll change its name to something that represents your solution. Inside, it packs a lot of thoughtful and standard optimizations that will save you countless hours doing what others have done before.

Besides this component, there are also a bunch of reusable libraries like logger, error-handler and more. All sit together under a single root folder in a single Git repository - this popular structure is called a 'Monorepo'.

Monorepos A typical Monorepo structure

The code inside is coded with Node.js, TypeScript, express and Postgresql. Later version of Practica.js will support more frameworks.

Running and testing the solution​

A minute before we start coding, let's ensure the solution starts and the tests pass. This will give us confidence to add more and more code knowing that we have a valid checkpoint (and tests to watch our back).

Just run the following standard commands:

  1. CD into the solution folder
cd {your-solution-folder}
  1. Install the right Node.js version
nvm use
  1. Install dependencies
npm install
  1. Run the tests
npm test

Tests pass? Great! πŸ₯³βœ…

They fail? oppss, this does not happen too often. Please approach our discord or open an issue in Github? We will try to assist shortly

  1. Optional: Start the app and check with Postman

Some rely on testing only, others like also to invoke routes using POSTMAN and test manually. We're good with both approach and recommend down the road to rely more and more on testing. Practica includes testing templates that are easy to write

Start the process first by navigating to the example component (order-service):

cd services/order-service

Start the DB using Docker and install tables (migration):

docker-compose -f ./test/docker-compose.yml up
npm run db:migrate

This step is not necessary for running tests as it will happen automatically

Then start the app:

npm start

Now visit our online POSTMAN collection, explore the routes, invoke and make yourself familiar with the app

Note: The API routes authorize requests, a valid token must be provided. You may generate one yourself (see here how), or just use the default development token that we generated for you πŸ‘‡. Put it inside an 'Authorization' header:

Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjIwMTY5NjIsImV4cCI6MTY5MzU1Mjk2MiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.65ACAjHy2ZE5i_uS5hyiEkOQfkqOqdj-WtBm-w23qZQ

We have the ground ready πŸ₯. Let's code now, just remember to run the tests (or POSTMAN) once in a while to ensure nothing breaks

The 3 layers of a component​

A typical component (e.g., Microservice) contains 3 main layers. This is a known and powerful pattern that is called "3-Tiers". It's an architectural structure that strikes a great balance between simplicity and robustness. Unlike other fancy architectures (e.g. hexagonal architecture, etc), this style is more likely to keep things simple and organized. The three layers represent the physical flow of a request with no abstractions:

Monorepos A typical Monorepo structure

- Layer 1: Entry points - This is the door to the application where flows start and requests come-in. Our example component has a REST API (i.e., API controllers), this is one kind of an entry-point. There might be other entry-points like a scheduled job, CLI, message queue and more. Whatever entry-point you're dealing with, the responsibility of this layer is minimal - receive requests, perform authentication, pass the request to be handled by the internal code and handle errors. For example, a controller gets an API request then it does nothing more than authenticating the user, extract the payload and call a domain layer function πŸ‘‡

- Domain - A folder containing the heart of the app where the flows, logic and data-structure are defined. Its functions can serve any type of entry-points - whether it's being called from API or message queue, the domain layer is agnostic to the source of the caller. Code here may call other services via HTTP/queue. It's likely also to fetch from and save information in a DB, for this it will call the data-access layer πŸ‘‡

- Data-access - Your entire DB interaction functionality and configuration is kept in this folder. For now, Practica.js uses ORM to interact with the DB - we're still debating on this decision

Now that you understand the structure of the example component, it's much easier to code over it πŸ‘‡

Let's code a flow from API to DB and in return​

We're about to implement a simple feature to make you familiar with the major code areas. After reading/coding this section, you should be able to add routes, logic and DB objects to your system easily. The example app deals with an imaginary e-commerce app. It has functionality for adding and querying for Orders. Goes without words that you'll change this to the entities and columns that represent your app.

πŸ— Key insight: Practica has no hidden abstractions, you have to become familiar with the (popular) chosen libraries. This minimizes future scenarios where you get stuck when an abstraction is not suitable to your need or you don't understand how things work.

Requirements - - Our missions is to code the following: Allow updating an order through the API. Orders should also have a new field: Status. When trying to edit an existing order, if the field order.'paymentTermsInDays' is 0 (i.e., the payment due date is now) or the order.status is 'delivered' - no changes are allowed and the code should return HTTP status 400 (bad request). Otherwise, we should update the DB with new order information

1. Change the example component/service name

Obviously your solution, has a different context and name. You probably want to rename the example service name from 'order-service' to {your-component-name}. Change both the folder name ('order-service') and the package.json name field:

./services/order-service/package.json

{
"name": "your-name-here",
"version": "0.0.2",
"description": "An example Node.js app that is packed with best practices",
}

If you're just experimenting with Practica, you may leave the name as-is for now.

2. Add a new 'Edit' route

The express API routes are located in the entry-points layer, in the file 'routes.ts': [root]/services/order-service/entry-points/api/routes.ts

This is a very typical express code, if you're familiar with express you'll be productive right away. This is a core principle of Practica - it uses battle tested technologies as-is. Let's just add a new route in this file:

// A new route to edit order
router.put('/:id', async (req, res, next) => {
try {
logger.info(`Order API was called to edit order ${req.params.id}`);
// Later on we will call the main code in the domain layer
// Fow now let's put hard coded values
res.json({id:1, userId: 1, productId: 2, countryId: 1,
deliveryAddress: '123 Main St, New York',
paymentTermsInDays: 30}).status(200).end();
} catch (err) {
next(err);
}
});

βœ…Best practice: The API entry-point (controller) should stay thin and focus on forwarding the request to the domain layer.

Looks highly familiar, right? If not, it means you should learn first how to code first with your preferred framework - in this case it's Express. That's the thing with Practica - We don't replace neither abstract your reputable framework, we only augment it.

3. Test your first route

Commonly, once we have a first code skeleton, it's time to start testing it. In Practica we recommend writing 'component tests' against the API and including all the layers (no mocking), we have great examples for this under [root]/services/order-service/test

You may visit the file: [root]/services/order-service/test/add-order.test.ts, read one of the test and you're likely to get the intent shortly. Our testing guide will be released shortly.

πŸ— Key insight: Practica's testing strategy is based on 'component tests' that include all the layers including the DB using docker-compose. We include rich testing patterns that mitigate various real-world risks like testing error handling, integrations and other things beyond the basics. Thanks to thoughtful setup, we're able to run 50 tests with DB in ~6 seconds. This is considered as a modern and highly-efficient strategy for testing Microservices

In this guide though, we're more focused on features craft - it's OK for now to test with POSTMAN or any other API explorer tool.

4. Create a DTO and a validation function

We're about to receive a payload from the caller, the edited order JSON. We obviously want to declare a strong schema/type so we can validate the incoming payloads and work with strong TypeScript types

βœ…Best practice: Validate incoming request and fail early. Both in run-time and development time

To meet these goals, we use two popular and powerful libraries: typebox and ajv. The first library, Typebox allows defining a schema with two outputs: TypeScript type and also JSON Schema. This is a standard and popular format that can be reused in many other places (e.g., to define OpenAPI spec). Based on this, the second library, ajv, will validate the requests.

Open the file [root]/services/order-service/domain/order-schema.ts

// Declare the basic order schema
import { Static, Type } from '@sinclair/typebox';
export const orderSchema = Type.Object({
deliveryAddress: Type.String(),
paymentTermsInDays: Type.Number(),
productId: Type.Integer(),
userId: Type.Integer(),
status: Type.Optional(Type.String()), // πŸ‘ˆ Add this field
});

This is Typebox's syntax for defines the basic order schema. Based on this we can get both JSON Schema and TypeScript type (!), this allows both run-time and development time protection. Add the status field to it and the following line to get a TypeScript type:

// This is a standard TypeScript type - we can use it now in the code and get intellisense + Typescript build-time validation
export type editOrderDTO = Static<typeof orderSchema>;

We have now strong development types to work with, it's time to configure our runtime validator. The library ajv gets JSON Schema, and validates the payload against it.

In the same file, let's define a validation function for edited orders:

// [root]/services/order-service/domain/order-schema
import { ajv } from '@practica/validation';
export function editOrderValidator() {
// For performance reason we cache the compiled validator function
const validator = ajv.getSchema<editOrderDTO>('edit-order');
if (!validator) {
ajv.addSchema(editOrderSchema, 'edit-order');
}

return ajv.getSchema<editOrderDTO>('edit-order')!;
}

We now have a TypeScript type and a function that can validate it on run-time. Knowing that we have safe types, it's time for the 'main thing' - coding the flow and logic

5. Create a use case (what the heck is 'use case'?)

Let's code our logic, but where? Obviously not in the controller/route which merely forwards request to our business logic layer. This should be done inside our domain folder, where the logic lives. Let's create a special type of code object - a use case.

A use-case is a plain JavaScript object/class which is created for every flow/feature. It summarizes the flow in a business and simple language without delving into the technical and small details. It mostly orchestrates other small services that hold all the implementation details. With use cases, the reader can grasp the high-level flow easily and avoid exposure to unnecessary complexity.

Let's add a new file inside the domain layer: edit-order-use-case.ts, and code the requirements:

// [root]/services/order-service/domain/edit-order-use-case.ts
import * as orderRepository from '../data-access/repositories/order-repository';

export default async function editOrder(orderId: number, updatedOrder: editOrderDTO) {
// Note how we use πŸ‘† the editOrderDTO that was defined in the previous step
assertOrderIsValid(updatedOrder);
assertEditingIsAllowed(updatedOrder.status, updatedOrder.paymentTermsInDays);
// Call the DB layer here πŸ‘‡ - to be explained soon
return await orderRepository.editOrder(orderId, updatedOrder);
}

Note how reading this function above easily tells the flow without messing with too much details. This is where use cases shine - by summarizing long details.

βœ…Best practice: Describe every feature/flow with a 'use case' object that summarizes the flow for better readability

Now we need to implement the functions that the use case calls. Since this is just a simple demo, we can put everything inside the use case. Consider a real-world scenario with heavier logic, calls to 3rd parties and DB work - In this case you'll need to spread this code across multiple services.

// [root]/services/order-service/domain/edit-order-use-case.ts
import { AppError } from '@practica/error-handling';
import { ajv } from '@practica/validation';
import { editOrderDTO, addOrderSchema } from './order-schema';

function assertOrderIsValid(updatedOrder: editOrderDTO) {
const isValid = ajv.validate(addOrderSchema, updatedOrder);
if (isValid === false) {
throw new AppError('invalid-order', `Validation failed`, 400, true);
}
}

function assertEditingIsAllowed( status: string | undefined,
paymentTermsInDays: number) {
if (status === 'delivered' || paymentTermsInDays === 0) {
throw new AppError(
'changes-not-allowed',
`It's not allow to delivered or paid orders`,
409, true);
}
}

πŸ— Key insight: Note how everything we did thus far is mostly coding functions. No fancy constructs, no abstractions, not even classes - we try to keep things as simple as possible. You may of course use other language features when the need arises. We suggest by-default to stick to plain functions and use other constructs when a strong need is identified.

6. Put the data access code

We're tasked with saving the edited order in the database. Any DB-related code is located within the folder: [root]/services/order-service/data-access.

Practica supports two popular ORM, Sequelize (default) and Prisma. Whatever you chose, both are a battle-tested and reputable option that will surely serve you well as long as the DB complexity is not overwhelming.

Before discussing the ORM-side, we wrap the entire DB layer with a simple class that externalizes all the DB functions to the domain layer. This is the repository pattern which advocates decoupling the DB narratives from the one who codes business logic. Inside [root]/services/order-service/data-access/repositories, you'll find a file 'order-repository', open it and add a new function:

[root]/services/order-service/data-access/order-repository.js
import { getOrderModel } from './models/order-model';// πŸ‘ˆ This is the ORM code which will get explained soon

export async function editOrder(orderId: number, orderDetails): OrderRecord {
const orderEditingResponse = await getOrderModel().update(orderDetails, {
where: { id: orderId },
});

return orderEditingResponse;
}

Note that this file contains a type - OrderRecord. This is a plain JS object (POJO) that is used to interact with the data access layer. This approach prevents leaking DB/ORM narratives to the domain layer (e.g., ActiveRecord style)

βœ…Best practice: Externalize any DB data with a response that contains plain JavaScript objects (the repository pattern)

Add the new Status field to this type:

type OrderRecord = {
id: number;
// ... other existing fields
status: string;// πŸ‘ˆ Add this field per our requirements
};

Let's configure the ORM now and define the Order model - a mapper between JavaScript object and a database table (a common ORM notion). Open the file [root]/services/order-service/data-access/models/order-model.ts:

import { DataTypes } from 'sequelize';
import getDbConnection from '../db-connection';

export default function getOrderModel() {
// getDbConnection returns a singleton Sequelize (ORM) object - This is necessary to avoid multiple DB connection pools
return getDbConnection().define('Order', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
deliveryAddress: {
type: DataTypes.STRING,
},
//some other fields here
status: {
type: DataTypes.String,// πŸ‘ˆ Add this field per our requirements
allowNull: true
}
});
}

This file defines the mapping between our received and returned JavaScript object and the database. Given this definition, the ORM can now expose functions to interact with data.

7. πŸ₯³ You have a robust working flow now

You should now be able to run the automated tests or POSTMAN and see the full flow working. It might feel like an overkill to create multiple layers and objects - naturally this level of modularization pays off when things get more complicated in real-world scenarios. Follow these layers and principles to write great code. In a short time, once you become familiar with these techniques - it will feel quick and natural

πŸ— Key insight: Anything we went through in this article is not unique to Practica.js rather ubiquitous backend concepts. Practica.js brings no overhead beyond the common best practices. This knowledge will serve you in any other scenario, regardless of Practica.js

We will be grateful if you share with us how to make this guide better

  • Ideas for future iterations: How to work with the Monorepo commands, Focus on a single componenent or run commands from the root, DB migration
- + \ No newline at end of file diff --git a/the-basics/getting-started-quickly/index.html b/the-basics/getting-started-quickly/index.html index 31ed7e25..1da70f60 100644 --- a/the-basics/getting-started-quickly/index.html +++ b/the-basics/getting-started-quickly/index.html @@ -9,13 +9,13 @@ - +

getting-started-quickly

Run Practica.js from the Command Line​

Run practica CLI and generate our default app (you can customize it using different flags):

npx @practica/create-node-app immediate --install-dependencies

✨ And you're done! That's it. The code's all been generated.

We also have a CLI interactive mode:

npx @practica/create-node-app interactive

Note that for now, it can generate an app that is based on Express and PostgreSQL only. Other options will get added soon


Start the Project​

cd {your chosen folder name}
npm install

Then choose whether to start the app:

npm run

or run the tests:

npm test

Pretty straight forward, right?

You just got a Node.js Monorepo solution with one example component/Microservice and multiple libraries. Based on this hardened solution you can build a robust application. The example component/Microservice is located under: {your chosen folder name}/services/order-service. This is where you'll find the API and a good spot to start your journey from.


Next Steps​

- + \ No newline at end of file diff --git a/the-basics/what-is-practica/index.html b/the-basics/what-is-practica/index.html index b4668d65..38008020 100644 --- a/the-basics/what-is-practica/index.html +++ b/the-basics/what-is-practica/index.html @@ -9,13 +9,13 @@ - +

What is practica.js

A One Paragraph Overview​

Although Node.js has great frameworks πŸ’š, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are neatly and thoughtfully documented. We strive to keep things as simple and standard as possible and base our work off the popular guide: Node.js Best Practices

1 min video πŸ‘‡

Our Philosophies and Unique Values​

1. Best Practices on top of known Node.js frameworks​

We don't re-invent the wheel. Rather, we use your favorite framework and empower it with structure and real examples. With a single command you can get an Express/Fastify-based codebase with ~100 examples of best practices inside.

Built on top of known frameworks

2. Simplicity, how Node.js was intended​

Keeping it simple, flat and based on native Node/JS capabilities is part of this project DNA. We believe that too many abstractions, high-complexity or fancy language features can quickly become a stumbling block for the team.

To name a few examples, our code flow is flat with almost no level of indirection, although using TypeScript - almost no features are being used besides types, for modularization we simply use Node.js modules

Built on top of known frameworks

3. Supports many technologies and frameworks​

Good Practices and Simplicity is the name of the game with Practica. There is no need to narrow our code to a specific framework or database. We aim to support a majority of popular Node.js frameworks and databases.

Built on top of known frameworks


Practices and Features

We apply more than 100 practices and optimizations. You can opt in or out for most of these features using option flags on our CLI. The follow table is just a few examples of features we provide. To see the full list of features, please visit our website here.

FeatureExplanationFlagDocs
Monorepo setupGenerates two components (e.g., Microservices) in a single repository with interactions between the two--mr, --monorepoDocs coming soon
Output escaping and sanitizingClean-out outgoing responses from potential HTML security risks like XSS--oe, --output-escapeDocs coming soon
Integration (component) testingGenerates full-blown component/integration tests setup including DB--t, --testsDocs coming soon
Unique request ID (Correlation ID)Generates module that creates a unique correlation/request ID for every incoming request. This is available for any other object during the request life-span. Internally it uses Node's built-in AsyncLocalStorage--coi, --correlation-idDocs coming soon
DockerfileGenerates dockerfile that embodies 20> best practices--df, --docker-fileDocs coming soon
Strong-schema configurationA configuration module that dynamically load run-time configuration keys and includes a strong schema so it can fail fastBuilt-in with basic appDocs here

πŸ“— See our full list of features here

- + \ No newline at end of file