[{"data":1,"prerenderedAt":3518},["ShallowReactive",2],{"article-tags-embedded":3},[4,841,876,3480,3500],{"id":5,"title":6,"body":7,"categories":807,"cover":809,"date":810,"description":811,"extension":812,"keywords":813,"meta":824,"navigation":825,"path":826,"published":827,"seo":828,"stem":829,"tags":830,"__hash__":840},"blog/blog/building-a-smart-heating-controller-with-esp32-s3.md","Building a smart heating controller for my house with ESP32-S3",{"type":8,"value":9,"toc":795},"minimark",[10,19,26,29,36,39,81,88,94,101,149,155,162,168,175,186,189,194,197,203,219,223,230,236,247,280,287,321,335,346,352,355,375,381,385,391,405,416,476,487,494,500,503,510,516,523,529,532,558,564,571,575,586,592,598,601,606,620,624,649,653,660,664,687,698,704,708,719,725,758,764,782,788,791],[11,12,13,14,18],"p",{},"My house in Sajkas is 220m2 over two floors: a ground floor with floor heating, an attic with its own zones, and a few fan-coil units. The plan is a ",[15,16,17],"strong",{},"hybrid heating system",": a Midea heat pump as the main source and a Romstal EcoHeat gas condensing boiler as backup. Both feed a buffer tank, and three circulation pumps move the heat from there into the house.",[11,20,21,22,25],{},"Off-the-shelf controllers either handle only one heat source, or they don't know anything about ",[15,23,24],{},"Serbian EPS tariffs"," (cheap night power from 23:00 to 07:00, and green/blue/red zones based on monthly consumption). So I built my own.",[11,27,28],{},"This post covers the architecture, the parts I'm happy with, and the hardware surprises that cost me the most time.",[30,31,33],"h1",{"id":32},"the-problem",[15,34,35],{},"The problem",[11,37,38],{},"The controller has to decide, at any moment:",[40,41,42,49,63,69,75],"ul",{},[43,44,45,48],"li",{},[15,46,47],{},"Is there heat demand?"," The ground floor thermostat, the attic zone actuators and each fan-coil unit report demand separately.",[43,50,51,54,55,58,59,62],{},[15,52,53],{},"Which source to use?"," The heat pump is preferred while it's efficient. Below ",[15,56,57],{},"-10C"," gas is allowed to help, and below ",[15,60,61],{},"-15C"," gas becomes the main source.",[43,64,65,68],{},[15,66,67],{},"How much does power cost right now?"," Night tariff is 50% of the day price, and the red consumption zone is expensive enough that gas is always worth considering there.",[43,70,71,74],{},[15,72,73],{},"Which pumps to run?"," Only the circuits that actually asked for heat.",[43,76,77,80],{},[15,78,79],{},"Is everything safe?"," Heat pump alarm, gas boiler alarm, flow monitoring, emergency stop, and a runtime watchdog.",[11,82,83,84,87],{},"All of that must keep working ",[15,85,86],{},"when WiFi is down",". Heating that stops because the router rebooted is not an option in January.",[30,89,91],{"id":90},"hardware",[15,92,93],{},"Hardware",[11,95,96],{},[97,98],"img",{"alt":99,"src":100},"Controller board on the bench","/img/articles/smart-home/board-bench.jpg",[40,102,103,109,115,121,127,133,143],{},[43,104,105,108],{},[15,106,107],{},"ESP32-S3 (N8R8)",": dual-core Xtensa LX7, 8MB flash, 8MB embedded Octal PSRAM",[43,110,111,114],{},[15,112,113],{},"8 relays",": heat pump enable, gas boiler (TA contact), DHW mode, DHW recirculation pump, ground floor pump, attic pump, fan-coil pump",[43,116,117,120],{},[15,118,119],{},"DS18B20"," 1-Wire probes on the DHW recirculation line and as clamp-on sensors on the buffer supply/return pipes",[43,122,123,126],{},[15,124,125],{},"SSD1306 OLED"," (128x64, I2C) with three rotating status pages",[43,128,129,132],{},[15,130,131],{},"Onboard WS2812 RGB LED"," that shows the boot and connectivity phase",[43,134,135,138,139,142],{},[15,136,137],{},"SALUS KL06-M"," wiring center for the attic floor heating zones, with a ",[15,140,141],{},"SALUS PL07"," pump-demand module",[43,144,145,148],{},[15,146,147],{},"Shelly 1PM Mini Gen3"," devices that bridge external demand signals (fan-coil units, attic zone controller) into MQTT",[11,150,151],{},[97,152],{"alt":153,"src":154},"Relays and wiring in the cabinet","/img/articles/smart-home/cabinet-wiring.jpg",[11,156,157,158,161],{},"The gas boiler has ",[15,159,160],{},"no Modbus and no OpenTherm",", only dry-contact inputs. So the ESP32 controls it the same way a room thermostat would: one relay on the TA terminals. Closed means \"I need heat\", and the boiler handles modulation, flow temperature and its own DHW priority on its own.",[30,163,165],{"id":164},"architecture-two-cores-two-worlds",[15,166,167],{},"Architecture: two cores, two worlds",[11,169,170,171,174],{},"The main design rule: ",[15,172,173],{},"the control loop never waits for the network."," The ESP32-S3 has two cores, so each one gets its own job:",[176,177,182],"pre",{"className":178,"code":180,"language":181},[179],"language-text","Core 1 (real-time control):              Core 0 (networking):\n  taskControlLoop  [prio 5, 8KB]           taskWiFiManager  [prio 3, 4KB]\n    sensors, FSM, relays, safety           taskMQTTClient   [prio 3, 8KB]\n    energy calc, 1000ms cycle              taskNTPSync      [prio 2, 4KB]\n  taskDisplayUpdate [prio 4, 4KB]          taskOTAHandler   [prio 1, 4KB]\n    I2C OLED every 3s\n","text",[183,184,180],"code",{"__ignoreMap":185},"",[11,187,188],{},"Core 1 reads sensors, runs the state machine and switches relays every second. Core 0 deals with WiFi, MQTT, NTP and OTA updates. The two sides only talk through FreeRTOS queues. An MQTT command (for example \"switch to summer mode\") goes into a command queue, and the control loop picks it up on its next cycle. Nothing on Core 1 ever blocks on a socket.",[190,191,193],"h2",{"id":192},"state-machine","State machine",[11,195,196],{},"The controller is a finite state machine with clear enter/exit actions:",[176,198,201],{"className":199,"code":200,"language":181},[179],"INIT -> IDLE -> HEATING_SPACE / GAS_SPACE / COOLING_SPACE\n                     |\n                  DEFROST\n        ALARM / EMERGENCY_STOP (from any state)\n",[183,202,200],{"__ignoreMap":185},[11,204,205,206,209,210,214,215,218],{},"Every transition has a ",[15,207,208],{},"reason code",". The dashboard and the logs always show ",[211,212,213],"em",{},"why"," the controller did something, not just ",[211,216,217],{},"what"," it did. That turned out to be the most useful debugging tool in the whole project.",[190,220,222],{"id":221},"anti-short-cycling","Anti short-cycling",[11,224,225,226,229],{},"Compressors and burners don't like being switched on and off every minute. The firmware enforces minimum run and stop times: 10 minutes run / 5 minutes stop for the heat pump, 5 minutes for the gas boiler, and a 3-minute delay before the heat pump changes mode so the refrigerant pressure can equalize. There's also a ",[15,227,228],{},"bench test mode"," that shortens all of these to seconds, so I can test the whole flow on my desk with LEDs instead of real pumps.",[30,231,233],{"id":232},"decision-logic-in-pure-c-tested-on-my-laptop",[15,234,235],{},"Decision logic in pure C, tested on my laptop",[11,237,238,239,242,243,246],{},"The best decision in this project was moving all decision logic into one file, ",[183,240,241],{},"heating_logic.c",", written in ",[15,244,245],{},"plain C99 with no Arduino dependencies",". Functions get everything as parameters (temperatures, current hour, tariff zone, demand flags) and return a decision:",[176,248,252],{"className":249,"code":250,"language":251,"meta":185,"style":185},"language-c shiki shiki-themes synthwave-84","// Pure functions: no globals, no hardware, time passed in as a parameter\nbool atticPumpShouldRun(bool atticDemand, unsigned long demandRoseAtMs, unsigned long nowMs);\nbool resolveAtticDemand(bool shellyOnline, bool shellyDemand, bool manualDemand);\nbool anySpaceHeatDemand(const SystemStatus *status);\n","c",[183,253,254,262,268,274],{"__ignoreMap":185},[255,256,259],"span",{"class":257,"line":258},"line",1,[255,260,261],{},"// Pure functions: no globals, no hardware, time passed in as a parameter\n",[255,263,265],{"class":257,"line":264},2,[255,266,267],{},"bool atticPumpShouldRun(bool atticDemand, unsigned long demandRoseAtMs, unsigned long nowMs);\n",[255,269,271],{"class":257,"line":270},3,[255,272,273],{},"bool resolveAtticDemand(bool shellyOnline, bool shellyDemand, bool manualDemand);\n",[255,275,277],{"class":257,"line":276},4,[255,278,279],{},"bool anySpaceHeatDemand(const SystemStatus *status);\n",[11,281,282,283,286],{},"So the same code compiles for the ESP32 ",[15,284,285],{},"and"," for a native test binary on my Mac. Around 100 Unity unit tests cover tariff windows, heat source selection, defrost, pump gating and reason codes, and the whole suite runs in about a second:",[176,288,292],{"className":289,"code":290,"language":291,"meta":185,"style":185},"language-bash shiki shiki-themes synthwave-84","cd arduino/smart-ac-dwh/tests && make -f Makefile.native test\n","bash",[183,293,294],{"__ignoreMap":185},[255,295,296,300,304,307,311,315,318],{"class":257,"line":258},[255,297,299],{"class":298},"sgRlq","cd",[255,301,303],{"class":302},"soy-K"," arduino/smart-ac-dwh/tests",[255,305,306],{"class":302}," && ",[255,308,310],{"class":309},"sRQ1T","make",[255,312,314],{"class":313},"sSmKr"," -f",[255,316,317],{"class":302}," Makefile.native",[255,319,320],{"class":302}," test\n",[11,322,323,324,327,328,327,331,334],{},"Hardware code (",[183,325,326],{},"inputs.cpp",", ",[183,329,330],{},"outputs.cpp",[183,332,333],{},"mqtt_client.cpp",") stays thin and only translates between GPIO/MQTT and those pure functions. When I change the heating behaviour, I write a test first and only flash the board once the logic is right.",[11,336,337,338,341,342,345],{},"One example of a bug the tests caught: every hour-based decision (night tariff, for example) was originally made even when the clock wasn't synced yet. After a reboot without internet, ",[183,339,340],{},"millis()"," would pretend it was 00:something, so the controller thought it was in cheap night tariff. Now all hour-based decisions are gated on ",[183,343,344],{},"ntp_synced",".",[30,347,349],{"id":348},"demand-driven-pumps",[15,350,351],{},"Demand-driven pumps",[11,353,354],{},"The first version simply ran all the pumps all winter. It worked, but it wasted electricity and heat. Now each pump runs only when its own circuit asks for heat:",[40,356,357,363,369],{},[43,358,359,362],{},[15,360,361],{},"Ground floor",": the dry contact of a Romstal RWT15 wireless thermostat receiver, wired straight to a GPIO input.",[43,364,365,368],{},[15,366,367],{},"Fan-coils",": three Shelly 1PM Mini devices, one per unit. When a unit is on, its Shelly publishes that to MQTT.",[43,370,371,374],{},[15,372,373],{},"Attic",": a SALUS KL06-M wiring center with a PL07 pump-demand module, bridged to MQTT through a Shelly (details below).",[11,376,377],{},[97,378],{"alt":379,"src":380},"Shelly devices","/img/articles/smart-home/shelly-devices.jpg",[190,382,384],{"id":383},"attic-salus-kl06-m-and-pl07","Attic: SALUS KL06-M and PL07",[11,386,387,388,390],{},"The attic floor heating has 8 loops. 7 of them have thermostatic actuators, grouped into 6 zones on a ",[15,389,137],{}," wiring center. Each zone thermostat tells the KL06-M to open its actuators. The 8th loop (the bathroom) deliberately has no actuator and always stays open, so the pump always has somewhere to push water, even when every other zone is closed.",[11,392,393,394,397,398,400,401,404],{},"The problem: the KL06-M itself has ",[15,395,396],{},"no pump-demand output"," for the ESP32 to read. It only has a serial expansion connector for SALUS modules. Reverse-engineering that proprietary bus wasn't worth it, so I added the ",[15,399,141],{}," module. It aggregates all 6 zones into ",[15,402,403],{},"one dry contact",": closed means at least one zone is calling for heat.",[11,406,407,408,411,412,415],{},"That contact goes to the ",[15,409,410],{},"SW input"," of a Shelly 1PM Mini, configured as a \"switch\" input. The Shelly doesn't switch any load here. It just senses the contact and publishes its state to MQTT (",[183,413,414],{},"\u003Ctopic_prefix>/status/input:0","). The firmware turns that into the attic demand:",[176,417,419],{"className":249,"code":418,"language":251,"meta":185,"style":185},"// Attic pump waits for the zone actuators to open first\nbool atticPumpShouldRun(bool atticDemand, unsigned long demandRoseAtMs, unsigned long nowMs) {\n  if (!atticDemand) {\n    return false;\n  }\n  if (demandRoseAtMs == 0) {\n    return false;  // rising edge not recorded yet: treat as still opening\n  }\n  return (nowMs - demandRoseAtMs) >= (unsigned long)ATTIC_ACTUATOR_OPEN_DELAY_SEC * 1000UL;\n}\n",[183,420,421,426,431,436,441,447,453,459,464,470],{"__ignoreMap":185},[255,422,423],{"class":257,"line":258},[255,424,425],{},"// Attic pump waits for the zone actuators to open first\n",[255,427,428],{"class":257,"line":264},[255,429,430],{},"bool atticPumpShouldRun(bool atticDemand, unsigned long demandRoseAtMs, unsigned long nowMs) {\n",[255,432,433],{"class":257,"line":270},[255,434,435],{},"  if (!atticDemand) {\n",[255,437,438],{"class":257,"line":276},[255,439,440],{},"    return false;\n",[255,442,444],{"class":257,"line":443},5,[255,445,446],{},"  }\n",[255,448,450],{"class":257,"line":449},6,[255,451,452],{},"  if (demandRoseAtMs == 0) {\n",[255,454,456],{"class":257,"line":455},7,[255,457,458],{},"    return false;  // rising edge not recorded yet: treat as still opening\n",[255,460,462],{"class":257,"line":461},8,[255,463,446],{},[255,465,467],{"class":257,"line":466},9,[255,468,469],{},"  return (nowMs - demandRoseAtMs) >= (unsigned long)ATTIC_ACTUATOR_OPEN_DELAY_SEC * 1000UL;\n",[255,471,473],{"class":257,"line":472},10,[255,474,475],{},"}\n",[11,477,478,479,482,483,486],{},"Thermal actuators take a few minutes to open, so the pump starts ",[15,480,481],{},"180 seconds after"," demand appears and stops immediately when it clears. The heat source, on the other hand, is enabled on the ",[211,484,485],{},"raw"," demand. That way the water is already warm by the time the pump starts.",[11,488,489,490,493],{},"If the Shelly goes offline (WiFi or MQTT outage), the firmware falls back to a manual override sent over MQTT (",[183,491,492],{},"cmd/set_attic_demand","), so the attic stays controllable.",[11,495,496],{},[97,497],{"alt":498,"src":499},"SALUS KL06-M with the PL07 module","/img/articles/smart-home/salus-kl06m-pl07.jpg",[11,501,502],{},"The gas boiler TA contact closes only when at least one of those three circuits asks for heat, exactly like a real room thermostat would.",[11,504,505,506,509],{},"Why Shelly devices instead of more wires to the ESP32? Simple: ",[15,507,508],{},"the board ran out of free pins."," More on that below.",[30,511,513],{"id":512},"backend-and-dashboard",[15,514,515],{},"Backend and dashboard",[11,517,518,519,522],{},"Everything the controller knows goes to MQTT under ",[183,520,521],{},"smartac/sajkas/",":",[176,524,527],{"className":525,"code":526,"language":181},[179],"smartac/sajkas/\n  state                    # \"IDLE\", \"HEATING_SPACE\", ... (retained)\n  mode                     # \"WINTER\" | \"SUMMER\" (retained)\n  temps/{dhw,dhw/recirc,buffer/supply,buffer/return}\n  relays/{hp_enable,gas_boiler,pump_floor_ground,pump_floor_first,pump_fancoil,...}\n  demand/{fancoil_1,fancoil_2,fancoil_3,attic}\n  energy/{instant_power,daily,monthly,projected,tariff_zone,tariff_period}\n  safety/{hp_alarm,gas_alarm,flow_detected,emergency_stop}\n  system/{uptime,wifi_rssi,free_heap,ntp_synced,firmware_version,ip}\n  cmd/{set_state,set_mode,reboot,set_config,...}\n",[183,528,526],{"__ignoreMap":185},[11,530,531],{},"On a home server (an old MacBook Pro), Docker Compose runs four services:",[40,533,534,540,546,552],{},[43,535,536,539],{},[15,537,538],{},"Mosquitto",": the MQTT broker",[43,541,542,545],{},[15,543,544],{},"TimescaleDB",": Postgres with time-series hypertables for telemetry and logs",[43,547,548,551],{},[15,549,550],{},"NestJS API",": an MQTT bridge, REST endpoints and Socket.IO for live updates",[43,553,554,557],{},[15,555,556],{},"Nuxt 4 dashboard",": the current state, temperature trends, relays grouped by subsystem, history and logs",[11,559,560],{},[97,561],{"alt":562,"src":563},"Dashboard overview","/img/articles/smart-home/dashboard-overview.png",[11,565,566,567,570],{},"The whole thing is a pnpm monorepo, and shared TypeScript types live in ",[183,568,569],{},"packages/types",", so the backend and the frontend agree on the message format.",[190,572,574],{"id":573},"transparent-logging","Transparent logging",[11,576,577,578,581,582,585],{},"The firmware keeps a ",[15,579,580],{},"ring buffer of 8192 log entries in PSRAM"," (768 KB), sends every entry to MQTT and the backend writes them into TimescaleDB. The dashboard has a ",[183,583,584],{},"/logs"," page with live logs from the controller. When something strange happens at 3 AM, I can see exactly what the controller \"thought\" at that moment.",[11,587,588],{},[97,589],{"alt":590,"src":591},"Logs page","/img/articles/smart-home/dashboard-logs.png",[30,593,595],{"id":594},"hardware-gotchas-that-cost-me-the-most-time",[15,596,597],{},"Hardware gotchas that cost me the most time",[11,599,600],{},"Writing the code was the easy part. Here's what really ate my evenings.",[602,603,605],"h3",{"id":604},"serial-completely-silent","Serial completely silent",[11,607,608,609,612,613,616,617,619],{},"After flashing, ",[183,610,611],{},"Serial"," printed nothing, not even the boot banner. It looked like the board hung at boot. A multimeter on the GPIOs proved the app was running fine. The cause: on this board, ",[183,614,615],{},"CDCOnBoot=cdc"," is required in the FQBN for ",[183,618,611],{}," to work over native USB. Flashing worked anyway, because esptool uses the ROM's own USB path.",[602,621,623],{"id":622},"a-macro-that-silently-reverted-its-value","A macro that silently \"reverted\" its value",[11,625,626,627,630,631,634,635,637,638,641,642,645,646,345],{},"The RGB LED on my board is on GPIO38, but every build kept driving GPIO48. The source clearly said 38, and the ",[183,628,629],{},".bin"," on flash matched the build byte for byte. The cause: I named my macro ",[183,632,633],{},"PIN_RGB_LED",", and the ESP32 Arduino core already defines ",[183,636,633],{}," as 48 in a generic variant header that gets included ",[15,639,640],{},"after"," mine. The preprocessor allows the redefinition, only with a warning I never saw. Lesson: ",[15,643,644],{},"never name a project macro like a well-known core macro."," Now it's called ",[183,647,648],{},"PIN_STATUS_RGB_LED",[602,650,652],{"id":651},"two-pins-that-die-as-soon-as-you-plug-in-a-wire","Two pins that \"die\" as soon as you plug in a wire",[11,654,655,656,659],{},"The OLED never showed up on an I2C scan. GPIO21 and GPIO47 read HIGH while floating, but dropped to LOW as soon as ",[211,657,658],{},"any"," wire was plugged in. A wire-swap test proved the problem followed the pin and not the wire or the module: most likely a solder bridge to GND on the header. I moved I2C to GPIO40/48 and it worked right away.",[602,661,663],{"id":662},"psram-vs-alarm-inputs","PSRAM vs. alarm inputs",[11,665,666,667,670,671,674,675,678,679,682,683,686],{},"When I turned on Octal PSRAM (",[183,668,669],{},"PSRAM=opi",") for the log buffer, the board went into a watchdog reset loop every 5 seconds. ",[183,672,673],{},"espefuse.py"," confirmed the chip is an ",[15,676,677],{},"ESP32-S3R8 with embedded Octal PSRAM",", which reserves ",[15,680,681],{},"GPIO33-37"," for its data bus. The heat pump alarm, gas alarm and flow switch were on GPIO35, 36 and 37. ",[183,684,685],{},"pinMode()"," on those pins silently broke the GPIO matrix, and the next I2C call hung.",[11,688,689,690,693,694,697],{},"On top of that, my first attempt to move them used GPIO22/23, which are valid pins on the chip but ",[15,691,692],{},"don't exist on this board's header",". That gave a false gas alarm that looked completely real. Final fix: GPIO7/10/11. The board now has ",[15,695,696],{},"zero spare pins",", which is why every new input goes through Shelly and MQTT.",[11,699,700],{},[97,701],{"alt":702,"src":703},"Multimeter and board during debugging","/img/articles/smart-home/debugging.jpg",[602,705,707],{"id":706},"dht11-that-crashed-core-1","DHT11 that crashed Core 1",[11,709,710,711,714,715,718],{},"I briefly tested a cheap DHT11 for room/outdoor temperature. Its library bit-bangs the protocol ",[15,712,713],{},"with interrupts disabled",", and when the sensor didn't respond, Core 1 got ",[183,716,717],{},"Interrupt wdt timeout on CPU1"," and the whole controller rebooted in a loop, relays included. It's the same class of problem as \"never block the control loop on the network\", just coming from a GPIO library instead. Any bit-banged sensor now gets its own low-priority task on Core 0. (That DHT11 later literally melted, but that's another story.)",[30,720,722],{"id":721},"what-i-learned",[15,723,724],{},"What I learned",[726,727,728,734,740,746,752],"ol",{},[43,729,730,733],{},[15,731,732],{},"Split decisions from hardware."," Pure C functions plus native tests mean I can change the heating logic with confidence, without a board on my desk.",[43,735,736,739],{},[15,737,738],{},"The network is optional, heating isn't."," Two cores and queues between them give that guarantee almost for free.",[43,741,742,745],{},[15,743,744],{},"\"Valid GPIO on the chip\" and \"a pin I can use on this board\" are two different questions."," Check with eFuse tools and a multimeter before you wire anything.",[43,747,748,751],{},[15,749,750],{},"Log the reason, not just the state."," A reason code for every transition saved me more time than any debugger.",[43,753,754,757],{},[15,755,756],{},"When you run out of pins, MQTT is your extension bus."," Shelly devices as cheap WiFi inputs proved to be a clean solution.",[30,759,761],{"id":760},"whats-next",[15,762,763],{},"What's next",[40,765,766,773,776,779],{},[43,767,768,769,772],{},"Installing the Midea heat pump and integrating it over ",[15,770,771],{},"Modbus RTU"," (reading COP, leaving water temperature and defrost status directly from the unit)",[43,774,775],{},"Outdoor and room temperature from an external WiFi station over MQTT",[43,777,778],{},"Summer cooling through the buffer tank, once the heat pump is in place",[43,780,781],{},"Optimizing the run schedule around the night tariff, based on real consumption data from TimescaleDB",[11,783,784],{},[97,785],{"alt":786,"src":787},"The system in the boiler room","/img/articles/smart-home/boiler-room.jpg",[11,789,790],{},"If you're building something similar, or you have a question about any part of this, feel free to reach out.",[792,793,794],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sgRlq, html code.shiki .sgRlq{--shiki-default:#36F9F6}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sRQ1T, html code.shiki .sRQ1T{--shiki-default:#FE4450}html pre.shiki code .sSmKr, html code.shiki .sSmKr{--shiki-default:#F97E72}",{"title":185,"searchDepth":443,"depth":443,"links":796},[797,798,799,800],{"id":192,"depth":264,"text":193},{"id":221,"depth":264,"text":222},{"id":383,"depth":264,"text":384},{"id":573,"depth":264,"text":574,"children":801},[802,803,804,805,806],{"id":604,"depth":270,"text":605},{"id":622,"depth":270,"text":623},{"id":651,"depth":270,"text":652},{"id":662,"depth":270,"text":663},{"id":706,"depth":270,"text":707},[808],"Embedded","/img/articles/smart-home/cover.jpg","2026-10-23","How I built my own controller for a hybrid heating system (heat pump + gas boiler) in a 220m2 house in Serbia: ESP32-S3 firmware with FreeRTOS on two cores, a pure C decision layer covered by native unit tests, MQTT, TimescaleDB, NestJS and a Nuxt dashboard, plus the hardware gotchas that cost me the most time.","md",[814,815,816,817,818,819,820,821,822,823],"ESP32-S3 heating controller","smart home heating","FreeRTOS dual core","heat pump gas boiler hybrid","MQTT home automation","EPS tariff optimization","TimescaleDB NestJS Nuxt","embedded C unit testing","Shelly MQTT","DIY home automation",{"enable_toc":825},true,"/blog/building-a-smart-heating-controller-with-esp32-s3",false,{"title":6,"description":811},"blog/building-a-smart-heating-controller-with-esp32-s3",[831,832,833,834,835,836,837,838,839],"esp32","embedded","freertos","mqtt","iot","smart-home","nestjs","nuxt","timescaledb","5rw-eot3Od4Z3Cg7OGzHxJw1NbVwbNBXcimBVMnzUqQ",{"id":842,"title":843,"body":844,"categories":851,"cover":853,"date":854,"description":855,"extension":812,"keywords":856,"meta":865,"navigation":825,"path":866,"published":827,"seo":867,"stem":868,"tags":869,"__hash__":875},"blog/blog/how-to-install-node-rdkafka-older-versions-on-apple-m-chips.md","How to install node-rdkafka older versions on Apple M chips",{"type":8,"value":845,"toc":849},[846],[11,847,848],{},"This is how to install node-rdkafka older versions on M1 Apple",{"title":185,"searchDepth":443,"depth":443,"links":850},[],[852],"Tset","/img/articles/integration-tests.png","2024-05-30","Explore various integration testing techniques for Node.js applications, covering key strategies, best practices, and implementation examples. This article delves into automated testing tools, mocking external services, and real-world test setups to ensure application quality and reliability.",[857,858,859,860,861,862,863,864],"Node.js integration testing","automated testing Node.js","integration test best practices","testing external services Node.js","mocking dependencies Node.js","Testcontainers Node.js","Node.js testing strategies","software testing Node.js",{"enable_toc":825},"/blog/how-to-install-node-rdkafka-older-versions-on-apple-m-chips",{"title":843,"description":855},"blog/how-to-install-node-rdkafka-older-versions-on-apple-m-chips",[870,837,871,872,873,874],"node","testing","integration-test","e2e-test","software-testing","sxKKrteiLanPQ7G0e6E6aSmRNJbsmNpYhUEJ4Mt4BoM",{"id":877,"title":878,"body":879,"categories":3470,"cover":853,"date":3472,"description":855,"extension":812,"keywords":3473,"meta":3474,"navigation":825,"path":3475,"published":825,"seo":3476,"stem":3477,"tags":3478,"__hash__":3479},"blog/blog/integration-testing-with-nodejs.md","Integration testing with NodeJS",{"type":8,"value":880,"toc":3468},[881,884,887,890,893,896,902,905,911,914,1158,1161,1318,1321,1681,1687,1690,1693,1696,1939,1942,2117,2120,2126,2129,2132,2135,2138,2141,2144,2943,2946,3411,3421,3427,3430,3433,3436,3450,3456,3459,3462,3465],[11,882,883],{},"Integration testing is a software testing technique that aims to test the interaction between different software components or modules when integrated. The purpose of integration testing is to ensure that the individual components of the software system work together correctly and produce the expected results.",[11,885,886],{},"Integration testing is typically performed after unit testing and before system testing. The focus of integration testing is on testing the interfaces between the different components and verifying that they function correctly as a whole. Integration testing can be performed using different strategies, such as top-down integration, bottom-up integration, and incremental integration.",[11,888,889],{},"In top-down integration, testing starts with the highest-level modules, and lower-level modules are gradually integrated and tested. In bottom-up integration, testing starts with the lowest-level modules, and higher-level modules are gradually integrated and tested. Incremental integration involves testing the system incrementally by adding and testing new modules one at a time.",[11,891,892],{},"Integration testing can be done manually or using automated testing tools. Automated testing tools can help streamline the integration testing process and reduce the time and effort required for testing. However, manual testing may still be necessary for certain situations to ensure that the software is functioning as intended.",[11,894,895],{},"Overall, integration testing is an essential part of the software testing process and helps to ensure the overall quality and reliability of the software system.",[30,897,899],{"id":898},"implementation-example",[15,900,901],{},"Implementation example",[11,903,904],{},"For example, we can take an application with two endpoints and also consider that complete flow requires other external services.",[11,906,907],{},[97,908],{"alt":909,"src":910},"image.png","https://www.interventure.info/wp-content/uploads/2023/06/image.png",[11,912,913],{},"Let’s say that we have our controller where the first method communicates with the database and the second endpoint calls external service:",[176,915,919],{"className":916,"code":917,"language":918,"meta":185,"style":185},"language-typescript shiki shiki-themes synthwave-84","// users.controller.ts\n@Controller('users')\nexport class UsersController {\n  constructor(private userService: UsersService, private userAuthService: UserAuthService) {}\n  @Get(':id')\n  async getById(@Param('id', new ParseUUIDPipe()) id: string) {\n    return this.userService.findById(id);\n  }\n  @Get('/:id/roles')\n  async getRolesById(@Param('id', new ParseUUIDPipe()) id: string) {\n    return this.userAuthService.getRolesByUserId(id);\n  }\n}\n","typescript",[183,920,921,927,945,960,994,1009,1050,1077,1081,1094,1125,1148,1153],{"__ignoreMap":185},[255,922,923],{"class":257,"line":258},[255,924,926],{"class":925},"sjThu","// users.controller.ts\n",[255,928,929,932,935,938,942],{"class":257,"line":264},[255,930,931],{"class":302},"@",[255,933,934],{"class":298},"Controller",[255,936,937],{"class":302},"(",[255,939,941],{"class":940},"sFl1-","'users'",[255,943,944],{"class":302},")\n",[255,946,947,951,954,957],{"class":257,"line":270},[255,948,950],{"class":949},"scrqX","export",[255,952,953],{"class":949}," class",[255,955,956],{"class":309}," UsersController",[255,958,959],{"class":302}," {\n",[255,961,962,965,967,970,974,976,979,981,983,986,988,991],{"class":257,"line":276},[255,963,964],{"class":949},"  constructor",[255,966,937],{"class":302},[255,968,969],{"class":949},"private",[255,971,973],{"class":972},"sH7Js"," userService",[255,975,522],{"class":949},[255,977,978],{"class":309}," UsersService",[255,980,327],{"class":302},[255,982,969],{"class":949},[255,984,985],{"class":972}," userAuthService",[255,987,522],{"class":949},[255,989,990],{"class":309}," UserAuthService",[255,992,993],{"class":302},") {}\n",[255,995,996,999,1002,1004,1007],{"class":257,"line":443},[255,997,998],{"class":302},"  @",[255,1000,1001],{"class":298},"Get",[255,1003,937],{"class":302},[255,1005,1006],{"class":940},"':id'",[255,1008,944],{"class":302},[255,1010,1011,1014,1017,1020,1023,1025,1028,1030,1033,1036,1039,1042,1044,1047],{"class":257,"line":449},[255,1012,1013],{"class":949},"  async",[255,1015,1016],{"class":298}," getById",[255,1018,1019],{"class":302},"(@",[255,1021,1022],{"class":298},"Param",[255,1024,937],{"class":302},[255,1026,1027],{"class":940},"'id'",[255,1029,327],{"class":302},[255,1031,1032],{"class":949},"new",[255,1034,1035],{"class":298}," ParseUUIDPipe",[255,1037,1038],{"class":302},"()) ",[255,1040,1041],{"class":972},"id",[255,1043,522],{"class":949},[255,1045,1046],{"class":309}," string",[255,1048,1049],{"class":302},") {\n",[255,1051,1052,1055,1059,1061,1065,1067,1070,1072,1074],{"class":257,"line":455},[255,1053,1054],{"class":949},"    return",[255,1056,1058],{"class":1057},"swVBR"," this",[255,1060,345],{"class":302},[255,1062,1064],{"class":1063},"sqjNi","userService",[255,1066,345],{"class":302},[255,1068,1069],{"class":298},"findById",[255,1071,937],{"class":302},[255,1073,1041],{"class":1063},[255,1075,1076],{"class":302},");\n",[255,1078,1079],{"class":257,"line":461},[255,1080,446],{"class":302},[255,1082,1083,1085,1087,1089,1092],{"class":257,"line":466},[255,1084,998],{"class":302},[255,1086,1001],{"class":298},[255,1088,937],{"class":302},[255,1090,1091],{"class":940},"'/:id/roles'",[255,1093,944],{"class":302},[255,1095,1096,1098,1101,1103,1105,1107,1109,1111,1113,1115,1117,1119,1121,1123],{"class":257,"line":472},[255,1097,1013],{"class":949},[255,1099,1100],{"class":298}," getRolesById",[255,1102,1019],{"class":302},[255,1104,1022],{"class":298},[255,1106,937],{"class":302},[255,1108,1027],{"class":940},[255,1110,327],{"class":302},[255,1112,1032],{"class":949},[255,1114,1035],{"class":298},[255,1116,1038],{"class":302},[255,1118,1041],{"class":972},[255,1120,522],{"class":949},[255,1122,1046],{"class":309},[255,1124,1049],{"class":302},[255,1126,1128,1130,1132,1134,1137,1139,1142,1144,1146],{"class":257,"line":1127},11,[255,1129,1054],{"class":949},[255,1131,1058],{"class":1057},[255,1133,345],{"class":302},[255,1135,1136],{"class":1063},"userAuthService",[255,1138,345],{"class":302},[255,1140,1141],{"class":298},"getRolesByUserId",[255,1143,937],{"class":302},[255,1145,1041],{"class":1063},[255,1147,1076],{"class":302},[255,1149,1151],{"class":257,"line":1150},12,[255,1152,446],{"class":302},[255,1154,1156],{"class":257,"line":1155},13,[255,1157,475],{"class":302},[11,1159,1160],{},"In the service specification:",[176,1162,1164],{"className":916,"code":1163,"language":918,"meta":185,"style":185},"// users.service.ts\n@Injectable()\nexport class UsersService {\n  constructor(private readonly userRepository: UsersRepository) {}\n  async findById(id: string) {\n    const foundUser = await this.userRepository.getById(id);\n    if (!foundUser) {\n      throw new NotFoundException('User is not found');\n    }\n    return foundUser;\n  }\n}\n",[183,1165,1166,1171,1181,1191,1212,1229,1262,1278,1296,1301,1310,1314],{"__ignoreMap":185},[255,1167,1168],{"class":257,"line":258},[255,1169,1170],{"class":925},"// users.service.ts\n",[255,1172,1173,1175,1178],{"class":257,"line":264},[255,1174,931],{"class":302},[255,1176,1177],{"class":298},"Injectable",[255,1179,1180],{"class":302},"()\n",[255,1182,1183,1185,1187,1189],{"class":257,"line":270},[255,1184,950],{"class":949},[255,1186,953],{"class":949},[255,1188,978],{"class":309},[255,1190,959],{"class":302},[255,1192,1193,1195,1197,1199,1202,1205,1207,1210],{"class":257,"line":276},[255,1194,964],{"class":949},[255,1196,937],{"class":302},[255,1198,969],{"class":949},[255,1200,1201],{"class":949}," readonly",[255,1203,1204],{"class":972}," userRepository",[255,1206,522],{"class":949},[255,1208,1209],{"class":309}," UsersRepository",[255,1211,993],{"class":302},[255,1213,1214,1216,1219,1221,1223,1225,1227],{"class":257,"line":443},[255,1215,1013],{"class":949},[255,1217,1218],{"class":298}," findById",[255,1220,937],{"class":302},[255,1222,1041],{"class":972},[255,1224,522],{"class":949},[255,1226,1046],{"class":309},[255,1228,1049],{"class":302},[255,1230,1231,1234,1237,1241,1244,1246,1248,1251,1253,1256,1258,1260],{"class":257,"line":449},[255,1232,1233],{"class":949},"    const",[255,1235,1236],{"class":1063}," foundUser",[255,1238,1240],{"class":1239},"sW278"," =",[255,1242,1243],{"class":949}," await",[255,1245,1058],{"class":1057},[255,1247,345],{"class":302},[255,1249,1250],{"class":1063},"userRepository",[255,1252,345],{"class":302},[255,1254,1255],{"class":298},"getById",[255,1257,937],{"class":302},[255,1259,1041],{"class":1063},[255,1261,1076],{"class":302},[255,1263,1264,1267,1270,1273,1276],{"class":257,"line":455},[255,1265,1266],{"class":949},"    if",[255,1268,1269],{"class":302}," (",[255,1271,1272],{"class":949},"!",[255,1274,1275],{"class":1063},"foundUser",[255,1277,1049],{"class":302},[255,1279,1280,1283,1286,1289,1291,1294],{"class":257,"line":461},[255,1281,1282],{"class":949},"      throw",[255,1284,1285],{"class":949}," new",[255,1287,1288],{"class":298}," NotFoundException",[255,1290,937],{"class":302},[255,1292,1293],{"class":940},"'User is not found'",[255,1295,1076],{"class":302},[255,1297,1298],{"class":257,"line":466},[255,1299,1300],{"class":302},"    }\n",[255,1302,1303,1305,1307],{"class":257,"line":472},[255,1304,1054],{"class":949},[255,1306,1236],{"class":1063},[255,1308,1309],{"class":302},";\n",[255,1311,1312],{"class":257,"line":1127},[255,1313,446],{"class":302},[255,1315,1316],{"class":257,"line":1150},[255,1317,475],{"class":302},[11,1319,1320],{},"External service call:",[176,1322,1324],{"className":916,"code":1323,"language":918,"meta":185,"style":185},"// user-auth.service.ts\n@Injectable()\nexport class UserAuthService {\n  constructor(\n    private http: HttpService,\n    private configService: CustomConfigService,\n  ) {}\n\n  // Request to the external auth service\n  async getRolesByUserId(userId: string) {\n    try {\n      const data = await lastValueFrom(\n        this.http\n          .get(`${this.configService.USER_AUTH_SERVICE_URL}/roles/${userId}`)\n          .pipe(map((res) =&gt; res.data)),\n      );\n\n      return data;\n    } catch (error) {\n      if (error?.response?.status) {\n        throw new HttpException(error?.response?.data, error.response.status);\n      }\n\n      throw new InternalServerErrorException();\n    }\n  }\n}\n",[183,1325,1326,1331,1339,1349,1356,1372,1386,1391,1396,1401,1419,1426,1443,1453,1501,1545,1551,1556,1566,1582,1605,1642,1648,1653,1666,1671,1676],{"__ignoreMap":185},[255,1327,1328],{"class":257,"line":258},[255,1329,1330],{"class":925},"// user-auth.service.ts\n",[255,1332,1333,1335,1337],{"class":257,"line":264},[255,1334,931],{"class":302},[255,1336,1177],{"class":298},[255,1338,1180],{"class":302},[255,1340,1341,1343,1345,1347],{"class":257,"line":270},[255,1342,950],{"class":949},[255,1344,953],{"class":949},[255,1346,990],{"class":309},[255,1348,959],{"class":302},[255,1350,1351,1353],{"class":257,"line":276},[255,1352,964],{"class":949},[255,1354,1355],{"class":302},"(\n",[255,1357,1358,1361,1364,1366,1369],{"class":257,"line":443},[255,1359,1360],{"class":949},"    private",[255,1362,1363],{"class":972}," http",[255,1365,522],{"class":949},[255,1367,1368],{"class":309}," HttpService",[255,1370,1371],{"class":302},",\n",[255,1373,1374,1376,1379,1381,1384],{"class":257,"line":449},[255,1375,1360],{"class":949},[255,1377,1378],{"class":972}," configService",[255,1380,522],{"class":949},[255,1382,1383],{"class":309}," CustomConfigService",[255,1385,1371],{"class":302},[255,1387,1388],{"class":257,"line":455},[255,1389,1390],{"class":302},"  ) {}\n",[255,1392,1393],{"class":257,"line":461},[255,1394,1395],{"emptyLinePlaceholder":825},"\n",[255,1397,1398],{"class":257,"line":466},[255,1399,1400],{"class":925},"  // Request to the external auth service\n",[255,1402,1403,1405,1408,1410,1413,1415,1417],{"class":257,"line":472},[255,1404,1013],{"class":949},[255,1406,1407],{"class":298}," getRolesByUserId",[255,1409,937],{"class":302},[255,1411,1412],{"class":972},"userId",[255,1414,522],{"class":949},[255,1416,1046],{"class":309},[255,1418,1049],{"class":302},[255,1420,1421,1424],{"class":257,"line":1127},[255,1422,1423],{"class":949},"    try",[255,1425,959],{"class":302},[255,1427,1428,1431,1434,1436,1438,1441],{"class":257,"line":1150},[255,1429,1430],{"class":949},"      const",[255,1432,1433],{"class":1063}," data",[255,1435,1240],{"class":1239},[255,1437,1243],{"class":949},[255,1439,1440],{"class":298}," lastValueFrom",[255,1442,1355],{"class":302},[255,1444,1445,1448,1450],{"class":257,"line":1155},[255,1446,1447],{"class":1057},"        this",[255,1449,345],{"class":302},[255,1451,1452],{"class":1063},"http\n",[255,1454,1456,1459,1462,1464,1467,1471,1474,1477,1480,1482,1485,1488,1491,1493,1495,1497,1499],{"class":257,"line":1455},14,[255,1457,1458],{"class":302},"          .",[255,1460,1461],{"class":298},"get",[255,1463,937],{"class":302},[255,1465,1466],{"class":940},"`",[255,1468,1470],{"class":1469},"sfE7D","${",[255,1472,1473],{"class":1057},"this",[255,1475,345],{"class":1476},"s2hdG",[255,1478,1479],{"class":1063},"configService",[255,1481,345],{"class":1476},[255,1483,1484],{"class":1063},"USER_AUTH_SERVICE_URL",[255,1486,1487],{"class":1469},"}",[255,1489,1490],{"class":940},"/roles/",[255,1492,1470],{"class":1469},[255,1494,1412],{"class":1063},[255,1496,1487],{"class":1469},[255,1498,1466],{"class":940},[255,1500,944],{"class":302},[255,1502,1504,1506,1509,1511,1514,1517,1520,1523,1526,1529,1532,1535,1537,1539,1542],{"class":257,"line":1503},15,[255,1505,1458],{"class":302},[255,1507,1508],{"class":298},"pipe",[255,1510,937],{"class":302},[255,1512,1513],{"class":298},"map",[255,1515,1516],{"class":302},"((",[255,1518,1519],{"class":1063},"res",[255,1521,1522],{"class":302},") ",[255,1524,1525],{"class":1239},"=",[255,1527,1528],{"class":949},"&",[255,1530,1531],{"class":1063},"gt",[255,1533,1534],{"class":302},"; ",[255,1536,1519],{"class":1063},[255,1538,345],{"class":302},[255,1540,1541],{"class":1063},"data",[255,1543,1544],{"class":302},")),\n",[255,1546,1548],{"class":257,"line":1547},16,[255,1549,1550],{"class":302},"      );\n",[255,1552,1554],{"class":257,"line":1553},17,[255,1555,1395],{"emptyLinePlaceholder":825},[255,1557,1559,1562,1564],{"class":257,"line":1558},18,[255,1560,1561],{"class":949},"      return",[255,1563,1433],{"class":1063},[255,1565,1309],{"class":302},[255,1567,1569,1572,1575,1577,1580],{"class":257,"line":1568},19,[255,1570,1571],{"class":302},"    } ",[255,1573,1574],{"class":949},"catch",[255,1576,1269],{"class":302},[255,1578,1579],{"class":1063},"error",[255,1581,1049],{"class":302},[255,1583,1585,1588,1590,1592,1595,1598,1600,1603],{"class":257,"line":1584},20,[255,1586,1587],{"class":949},"      if",[255,1589,1269],{"class":302},[255,1591,1579],{"class":1063},[255,1593,1594],{"class":302},"?.",[255,1596,1597],{"class":1063},"response",[255,1599,1594],{"class":302},[255,1601,1602],{"class":1063},"status",[255,1604,1049],{"class":302},[255,1606,1608,1611,1613,1616,1618,1620,1622,1624,1626,1628,1630,1632,1634,1636,1638,1640],{"class":257,"line":1607},21,[255,1609,1610],{"class":949},"        throw",[255,1612,1285],{"class":949},[255,1614,1615],{"class":298}," HttpException",[255,1617,937],{"class":302},[255,1619,1579],{"class":1063},[255,1621,1594],{"class":302},[255,1623,1597],{"class":1063},[255,1625,1594],{"class":302},[255,1627,1541],{"class":1063},[255,1629,327],{"class":302},[255,1631,1579],{"class":1063},[255,1633,345],{"class":302},[255,1635,1597],{"class":1063},[255,1637,345],{"class":302},[255,1639,1602],{"class":1063},[255,1641,1076],{"class":302},[255,1643,1645],{"class":257,"line":1644},22,[255,1646,1647],{"class":302},"      }\n",[255,1649,1651],{"class":257,"line":1650},23,[255,1652,1395],{"emptyLinePlaceholder":825},[255,1654,1656,1658,1660,1663],{"class":257,"line":1655},24,[255,1657,1282],{"class":949},[255,1659,1285],{"class":949},[255,1661,1662],{"class":298}," InternalServerErrorException",[255,1664,1665],{"class":302},"();\n",[255,1667,1669],{"class":257,"line":1668},25,[255,1670,1300],{"class":302},[255,1672,1674],{"class":257,"line":1673},26,[255,1675,446],{"class":302},[255,1677,1679],{"class":257,"line":1678},27,[255,1680,475],{"class":302},[30,1682,1684],{"id":1683},"mocking-external-service-dependency",[15,1685,1686],{},"Mocking external service dependency",[11,1688,1689],{},"Mocking external dependencies involves replacing the real implementation of an external service with a mock object that simulates the expected behavior of the service. This allows the integration test to continue running without relying on the actual external service.",[11,1691,1692],{},"Mocking external dependencies can be done using various tools and libraries, such as SinonJS, Nock, or Jest. In this example, we created a simple mock by using only a small Express app, since Express comes with NestJS out of the box.",[11,1694,1695],{},"First, we setup a simple server wrapper:",[176,1697,1699],{"className":916,"code":1698,"language":918,"meta":185,"style":185},"export const createDummyServer = async (\n  registerEndpoints: (Express) => void,\n): Promise\u003CDummyServer> => {\n  const app = express();\n  app.use(express.json());\n  registerEndpoints(app);\n  return new Promise((resolve) => {\n    const server = app.listen(0, () => {\n      resolve({\n        url: `http://localhost:${(server.address() as AddressInfo).port}`,\n        close: () => server.close(),\n}); });\n",[183,1700,1701,1719,1741,1764,1779,1802,1813,1833,1861,1869,1912,1934],{"__ignoreMap":185},[255,1702,1703,1705,1708,1711,1713,1716],{"class":257,"line":258},[255,1704,950],{"class":949},[255,1706,1707],{"class":949}," const",[255,1709,1710],{"class":298}," createDummyServer",[255,1712,1240],{"class":1239},[255,1714,1715],{"class":949}," async",[255,1717,1718],{"class":302}," (\n",[255,1720,1721,1724,1726,1728,1731,1733,1736,1739],{"class":257,"line":264},[255,1722,1723],{"class":298},"  registerEndpoints",[255,1725,522],{"class":949},[255,1727,1269],{"class":302},[255,1729,1730],{"class":972},"Express",[255,1732,1522],{"class":302},[255,1734,1735],{"class":949},"=>",[255,1737,1738],{"class":309}," void",[255,1740,1371],{"class":302},[255,1742,1743,1746,1748,1751,1754,1757,1760,1762],{"class":257,"line":270},[255,1744,1745],{"class":302},")",[255,1747,522],{"class":949},[255,1749,1750],{"class":309}," Promise",[255,1752,1753],{"class":302},"\u003C",[255,1755,1756],{"class":309},"DummyServer",[255,1758,1759],{"class":302},"> ",[255,1761,1735],{"class":949},[255,1763,959],{"class":302},[255,1765,1766,1769,1772,1774,1777],{"class":257,"line":276},[255,1767,1768],{"class":949},"  const",[255,1770,1771],{"class":1063}," app",[255,1773,1240],{"class":1239},[255,1775,1776],{"class":298}," express",[255,1778,1665],{"class":302},[255,1780,1781,1784,1786,1789,1791,1794,1796,1799],{"class":257,"line":443},[255,1782,1783],{"class":1063},"  app",[255,1785,345],{"class":302},[255,1787,1788],{"class":298},"use",[255,1790,937],{"class":302},[255,1792,1793],{"class":1063},"express",[255,1795,345],{"class":302},[255,1797,1798],{"class":298},"json",[255,1800,1801],{"class":302},"());\n",[255,1803,1804,1806,1808,1811],{"class":257,"line":449},[255,1805,1723],{"class":298},[255,1807,937],{"class":302},[255,1809,1810],{"class":1063},"app",[255,1812,1076],{"class":302},[255,1814,1815,1818,1820,1822,1824,1827,1829,1831],{"class":257,"line":455},[255,1816,1817],{"class":949},"  return",[255,1819,1285],{"class":949},[255,1821,1750],{"class":309},[255,1823,1516],{"class":302},[255,1825,1826],{"class":972},"resolve",[255,1828,1522],{"class":302},[255,1830,1735],{"class":949},[255,1832,959],{"class":302},[255,1834,1835,1837,1840,1842,1844,1846,1849,1851,1854,1857,1859],{"class":257,"line":461},[255,1836,1233],{"class":949},[255,1838,1839],{"class":1063}," server",[255,1841,1240],{"class":1239},[255,1843,1771],{"class":1063},[255,1845,345],{"class":302},[255,1847,1848],{"class":298},"listen",[255,1850,937],{"class":302},[255,1852,1853],{"class":313},"0",[255,1855,1856],{"class":302},", () ",[255,1858,1735],{"class":949},[255,1860,959],{"class":302},[255,1862,1863,1866],{"class":257,"line":466},[255,1864,1865],{"class":298},"      resolve",[255,1867,1868],{"class":302},"({\n",[255,1870,1871,1874,1876,1879,1881,1883,1886,1888,1891,1894,1897,1900,1903,1906,1908,1910],{"class":257,"line":472},[255,1872,1873],{"class":1063},"        url",[255,1875,522],{"class":1476},[255,1877,1878],{"class":940}," `http://localhost:",[255,1880,1470],{"class":1469},[255,1882,937],{"class":1476},[255,1884,1885],{"class":1063},"server",[255,1887,345],{"class":1476},[255,1889,1890],{"class":298},"address",[255,1892,1893],{"class":1476},"() ",[255,1895,1896],{"class":949},"as",[255,1898,1899],{"class":309}," AddressInfo",[255,1901,1902],{"class":1476},").",[255,1904,1905],{"class":1063},"port",[255,1907,1487],{"class":1469},[255,1909,1466],{"class":940},[255,1911,1371],{"class":302},[255,1913,1914,1917,1919,1922,1924,1926,1928,1931],{"class":257,"line":1127},[255,1915,1916],{"class":298},"        close",[255,1918,522],{"class":1476},[255,1920,1921],{"class":302}," () ",[255,1923,1735],{"class":949},[255,1925,1839],{"class":1063},[255,1927,345],{"class":302},[255,1929,1930],{"class":298},"close",[255,1932,1933],{"class":302},"(),\n",[255,1935,1936],{"class":257,"line":1150},[255,1937,1938],{"class":302},"}); });\n",[11,1940,1941],{},"And finally, setup a mock for the user authorization service:",[176,1943,1945],{"className":916,"code":1944,"language":918,"meta":185,"style":185},"export const createDummyAuthServiceServer = async (): Promise&amp;lt;DummyServer&amp;gt; =&amp;gt; {\n  return createDummyServer((app) =&amp;gt; {\n    app.get('/roles/:userId', (req, res) =&amp;gt; {\n      if (req.params.userId !== 'b618445a-0089-43d5-b9ca-e6f2fc29a11d') {\n        return res.status(404).send('User not found');\n      }\n      res.json(userRoles);\n    });\n}); };\n",[183,1946,1947,2004,2019,2057,2069,2098,2102,2107,2112],{"__ignoreMap":185},[255,1948,1949,1951,1953,1956,1958,1960,1963,1965,1967,1969,1972,1975,1978,1980,1982,1984,1986,1988,1990,1993,1995,1997,1999,2001],{"class":257,"line":258},[255,1950,950],{"class":949},[255,1952,1707],{"class":949},[255,1954,1955],{"class":298}," createDummyAuthServiceServer",[255,1957,1240],{"class":1239},[255,1959,1715],{"class":949},[255,1961,1962],{"class":302}," ()",[255,1964,522],{"class":949},[255,1966,1750],{"class":309},[255,1968,1528],{"class":949},[255,1970,1971],{"class":309},"amp",[255,1973,1974],{"class":302},";",[255,1976,1977],{"class":309},"lt",[255,1979,1974],{"class":302},[255,1981,1756],{"class":309},[255,1983,1528],{"class":949},[255,1985,1971],{"class":309},[255,1987,1974],{"class":302},[255,1989,1531],{"class":309},[255,1991,1992],{"class":302},"; =",[255,1994,1528],{"class":949},[255,1996,1971],{"class":309},[255,1998,1974],{"class":302},[255,2000,1531],{"class":309},[255,2002,2003],{"class":302},"; {\n",[255,2005,2006,2009,2012,2014,2016],{"class":257,"line":264},[255,2007,2008],{"class":302},"  return ",[255,2010,2011],{"class":298},"createDummyServer",[255,2013,1516],{"class":302},[255,2015,1810],{"class":972},[255,2017,2018],{"class":302},") =&amp;gt; {\n",[255,2020,2021,2024,2026,2028,2030,2033,2036,2039,2041,2043,2045,2047,2049,2051,2053,2055],{"class":257,"line":270},[255,2022,2023],{"class":1063},"    app",[255,2025,345],{"class":302},[255,2027,1461],{"class":298},[255,2029,937],{"class":302},[255,2031,2032],{"class":940},"'/roles/:userId'",[255,2034,2035],{"class":302},", (",[255,2037,2038],{"class":1063},"req",[255,2040,327],{"class":302},[255,2042,1519],{"class":1063},[255,2044,1522],{"class":302},[255,2046,1525],{"class":1239},[255,2048,1528],{"class":949},[255,2050,1971],{"class":1063},[255,2052,1974],{"class":302},[255,2054,1531],{"class":1063},[255,2056,2003],{"class":302},[255,2058,2059,2061,2064,2067],{"class":257,"line":276},[255,2060,1587],{"class":298},[255,2062,2063],{"class":302}," (req.params.userId !== ",[255,2065,2066],{"class":940},"'b618445a-0089-43d5-b9ca-e6f2fc29a11d'",[255,2068,1049],{"class":302},[255,2070,2071,2074,2077,2079,2081,2083,2086,2088,2091,2093,2096],{"class":257,"line":443},[255,2072,2073],{"class":949},"        return",[255,2075,2076],{"class":1063}," res",[255,2078,345],{"class":302},[255,2080,1602],{"class":298},[255,2082,937],{"class":302},[255,2084,2085],{"class":313},"404",[255,2087,1902],{"class":302},[255,2089,2090],{"class":298},"send",[255,2092,937],{"class":302},[255,2094,2095],{"class":940},"'User not found'",[255,2097,1076],{"class":302},[255,2099,2100],{"class":257,"line":449},[255,2101,1647],{"class":302},[255,2103,2104],{"class":257,"line":455},[255,2105,2106],{"class":302},"      res.json(userRoles);\n",[255,2108,2109],{"class":257,"line":461},[255,2110,2111],{"class":302},"    });\n",[255,2113,2114],{"class":257,"line":466},[255,2115,2116],{"class":302},"}); };\n",[11,2118,2119],{},"As you can see, we are listening for a GET request on the specified route, performing a simple check of the user id, and finally, returning the mock value from a simple JSON file.",[30,2121,2123],{"id":2122},"setup-integration-tests",[15,2124,2125],{},"Setup integration tests",[11,2127,2128],{},"Tests for endpoints can be split into two parts. The first is related to the external dependencies setup.",[11,2130,2131],{},"The example below creates a mocked service and spins up the database using test containers.",[11,2133,2134],{},"Testcontainers is an open-source Java-based library that allows you to run software containers in your tests. The library provides lightweight, throwaway instances of common databases, web servers, and other services that you can use for integration testing.",[11,2136,2137],{},"The idea behind Testcontainers is to make it easier to test code that interacts with external systems, without having to worry about the complexities of setting up and tearing down those systems. By using containers, Testcontainers can spin up isolated instances of databases like PostgreSQL, MySQL, and Oracle, message brokers like Kafka and RabbitMQ, and web servers like Tomcat and Jetty.",[11,2139,2140],{},"The environment variables are set for before mentioned dependencies, and the leading service starts running.",[11,2142,2143],{},"The database is cleaned before every test run. External dependencies (mocked service and database) are closed after tests finish.",[176,2145,2147],{"className":916,"code":2146,"language":918,"meta":185,"style":185},"describe('UsersController (integration)', () => {\n  let app: INestApplication;\n  let dummyAuthServiceServerClose: () => void;\n  let postgresContainer: StartedTestContainer;\n  let usersRepository: Repository\u003CUsersEntity>;\n  const databaseConfig = {\n    databaseName: 'nestjs-starter-db',\n    databaseUsername: 'user',\n    databasePassword: 'some-r4ndom-pasS',\n    databasePort: 5432,\n};\n  beforeAll(async () => {\n    const dummyAuthServiceServer = await createDummyAuthServiceServer();\n    dummyAuthServiceServerClose = dummyAuthServiceServer.close;\n    postgresContainer = await new GenericContainer('postgres:15-alpine')\n      .withEnvironment({\n        POSTGRES_USER: databaseConfig.databaseUsername,POSTGRES_PASSWORD: databaseConfig.databasePassword,\n        POSTGRES_DB: databaseConfig.databaseName,\n      })\n      .withExposedPorts(databaseConfig.databasePort)\n      .start();\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      imports: [AppModule],\n    })\n      .overrideProvider(ConfigService)\n      .useValue({\n        get: (key: string): string => {\n          const map: Record\u003Cstring, string | undefined> = process.env;\n          map.USER_AUTH_SERVICE_URL = dummyAuthServiceServer.url;\n          map.DATABASE_HOSTNAME = postgresContainer.getHost();\n          map.DATABASE_PORT = `${postgresContainer.getMappedPort(\n            databaseConfig.databasePort,\n          )}`;\n          map.DATABASE_NAME = databaseConfig.databaseName;\n          map.DATABASE_USERNAME = databaseConfig.databaseUsername;\n          map.DATABASE_PASSWORD = databaseConfig.databasePassword;\n          return map[key] || '';\n        },\n      })\n      .compile();\n    app = moduleFixture.createNestApplication();\n    usersRepository = app.get(getRepositoryToken(UsersEntity));\n    await app.init();\n  });\n  beforeEach(async () => {\n    await usersRepository.delete({});\n  });\n  afterAll(async () => {\n    await app.close();\n    dummyAuthServiceServerClose();\n    await postgresContainer.stop();\n  });\n",[183,2148,2149,2165,2179,2196,2210,2230,2241,2253,2265,2277,2289,2294,2310,2325,2340,2361,2371,2402,2418,2423,2442,2451,2477,2493,2498,2512,2521,2548,2591,2612,2633,2660,2672,2684,2704,2724,2744,2768,2774,2779,2789,2805,2831,2846,2852,2868,2883,2888,2904,2917,2924,2938],{"__ignoreMap":185},[255,2150,2151,2154,2156,2159,2161,2163],{"class":257,"line":258},[255,2152,2153],{"class":298},"describe",[255,2155,937],{"class":302},[255,2157,2158],{"class":940},"'UsersController (integration)'",[255,2160,1856],{"class":302},[255,2162,1735],{"class":949},[255,2164,959],{"class":302},[255,2166,2167,2170,2172,2174,2177],{"class":257,"line":264},[255,2168,2169],{"class":949},"  let",[255,2171,1771],{"class":1063},[255,2173,522],{"class":949},[255,2175,2176],{"class":309}," INestApplication",[255,2178,1309],{"class":302},[255,2180,2181,2183,2186,2188,2190,2192,2194],{"class":257,"line":270},[255,2182,2169],{"class":949},[255,2184,2185],{"class":298}," dummyAuthServiceServerClose",[255,2187,522],{"class":949},[255,2189,1921],{"class":302},[255,2191,1735],{"class":949},[255,2193,1738],{"class":309},[255,2195,1309],{"class":302},[255,2197,2198,2200,2203,2205,2208],{"class":257,"line":276},[255,2199,2169],{"class":949},[255,2201,2202],{"class":1063}," postgresContainer",[255,2204,522],{"class":949},[255,2206,2207],{"class":309}," StartedTestContainer",[255,2209,1309],{"class":302},[255,2211,2212,2214,2217,2219,2222,2224,2227],{"class":257,"line":443},[255,2213,2169],{"class":949},[255,2215,2216],{"class":1063}," usersRepository",[255,2218,522],{"class":949},[255,2220,2221],{"class":309}," Repository",[255,2223,1753],{"class":302},[255,2225,2226],{"class":309},"UsersEntity",[255,2228,2229],{"class":302},">;\n",[255,2231,2232,2234,2237,2239],{"class":257,"line":449},[255,2233,1768],{"class":949},[255,2235,2236],{"class":1063}," databaseConfig",[255,2238,1240],{"class":1239},[255,2240,959],{"class":302},[255,2242,2243,2246,2248,2251],{"class":257,"line":455},[255,2244,2245],{"class":1063},"    databaseName",[255,2247,522],{"class":1476},[255,2249,2250],{"class":940}," 'nestjs-starter-db'",[255,2252,1371],{"class":302},[255,2254,2255,2258,2260,2263],{"class":257,"line":461},[255,2256,2257],{"class":1063},"    databaseUsername",[255,2259,522],{"class":1476},[255,2261,2262],{"class":940}," 'user'",[255,2264,1371],{"class":302},[255,2266,2267,2270,2272,2275],{"class":257,"line":466},[255,2268,2269],{"class":1063},"    databasePassword",[255,2271,522],{"class":1476},[255,2273,2274],{"class":940}," 'some-r4ndom-pasS'",[255,2276,1371],{"class":302},[255,2278,2279,2282,2284,2287],{"class":257,"line":472},[255,2280,2281],{"class":1063},"    databasePort",[255,2283,522],{"class":1476},[255,2285,2286],{"class":313}," 5432",[255,2288,1371],{"class":302},[255,2290,2291],{"class":257,"line":1127},[255,2292,2293],{"class":302},"};\n",[255,2295,2296,2299,2301,2304,2306,2308],{"class":257,"line":1150},[255,2297,2298],{"class":298},"  beforeAll",[255,2300,937],{"class":302},[255,2302,2303],{"class":949},"async",[255,2305,1921],{"class":302},[255,2307,1735],{"class":949},[255,2309,959],{"class":302},[255,2311,2312,2314,2317,2319,2321,2323],{"class":257,"line":1155},[255,2313,1233],{"class":949},[255,2315,2316],{"class":1063}," dummyAuthServiceServer",[255,2318,1240],{"class":1239},[255,2320,1243],{"class":949},[255,2322,1955],{"class":298},[255,2324,1665],{"class":302},[255,2326,2327,2330,2332,2334,2336,2338],{"class":257,"line":1455},[255,2328,2329],{"class":1063},"    dummyAuthServiceServerClose",[255,2331,1240],{"class":1239},[255,2333,2316],{"class":1063},[255,2335,345],{"class":302},[255,2337,1930],{"class":1063},[255,2339,1309],{"class":302},[255,2341,2342,2345,2347,2349,2351,2354,2356,2359],{"class":257,"line":1503},[255,2343,2344],{"class":1063},"    postgresContainer",[255,2346,1240],{"class":1239},[255,2348,1243],{"class":949},[255,2350,1285],{"class":949},[255,2352,2353],{"class":298}," GenericContainer",[255,2355,937],{"class":302},[255,2357,2358],{"class":940},"'postgres:15-alpine'",[255,2360,944],{"class":302},[255,2362,2363,2366,2369],{"class":257,"line":1547},[255,2364,2365],{"class":302},"      .",[255,2367,2368],{"class":298},"withEnvironment",[255,2370,1868],{"class":302},[255,2372,2373,2376,2378,2380,2382,2385,2388,2391,2393,2395,2397,2400],{"class":257,"line":1553},[255,2374,2375],{"class":1063},"        POSTGRES_USER",[255,2377,522],{"class":1476},[255,2379,2236],{"class":1063},[255,2381,345],{"class":302},[255,2383,2384],{"class":1063},"databaseUsername",[255,2386,2387],{"class":302},",",[255,2389,2390],{"class":1063},"POSTGRES_PASSWORD",[255,2392,522],{"class":1476},[255,2394,2236],{"class":1063},[255,2396,345],{"class":302},[255,2398,2399],{"class":1063},"databasePassword",[255,2401,1371],{"class":302},[255,2403,2404,2407,2409,2411,2413,2416],{"class":257,"line":1558},[255,2405,2406],{"class":1063},"        POSTGRES_DB",[255,2408,522],{"class":1476},[255,2410,2236],{"class":1063},[255,2412,345],{"class":302},[255,2414,2415],{"class":1063},"databaseName",[255,2417,1371],{"class":302},[255,2419,2420],{"class":257,"line":1568},[255,2421,2422],{"class":302},"      })\n",[255,2424,2425,2427,2430,2432,2435,2437,2440],{"class":257,"line":1584},[255,2426,2365],{"class":302},[255,2428,2429],{"class":298},"withExposedPorts",[255,2431,937],{"class":302},[255,2433,2434],{"class":1063},"databaseConfig",[255,2436,345],{"class":302},[255,2438,2439],{"class":1063},"databasePort",[255,2441,944],{"class":302},[255,2443,2444,2446,2449],{"class":257,"line":1607},[255,2445,2365],{"class":302},[255,2447,2448],{"class":298},"start",[255,2450,1665],{"class":302},[255,2452,2453,2455,2458,2460,2463,2465,2467,2470,2472,2475],{"class":257,"line":1644},[255,2454,1233],{"class":949},[255,2456,2457],{"class":1063}," moduleFixture",[255,2459,522],{"class":949},[255,2461,2462],{"class":309}," TestingModule",[255,2464,1240],{"class":1239},[255,2466,1243],{"class":949},[255,2468,2469],{"class":1063}," Test",[255,2471,345],{"class":302},[255,2473,2474],{"class":298},"createTestingModule",[255,2476,1868],{"class":302},[255,2478,2479,2482,2484,2487,2490],{"class":257,"line":1650},[255,2480,2481],{"class":1063},"      imports",[255,2483,522],{"class":1476},[255,2485,2486],{"class":302}," [",[255,2488,2489],{"class":1063},"AppModule",[255,2491,2492],{"class":302},"],\n",[255,2494,2495],{"class":257,"line":1655},[255,2496,2497],{"class":302},"    })\n",[255,2499,2500,2502,2505,2507,2510],{"class":257,"line":1668},[255,2501,2365],{"class":302},[255,2503,2504],{"class":298},"overrideProvider",[255,2506,937],{"class":302},[255,2508,2509],{"class":1063},"ConfigService",[255,2511,944],{"class":302},[255,2513,2514,2516,2519],{"class":257,"line":1673},[255,2515,2365],{"class":302},[255,2517,2518],{"class":298},"useValue",[255,2520,1868],{"class":302},[255,2522,2523,2526,2528,2530,2533,2535,2537,2539,2541,2543,2546],{"class":257,"line":1678},[255,2524,2525],{"class":298},"        get",[255,2527,522],{"class":1476},[255,2529,1269],{"class":302},[255,2531,2532],{"class":972},"key",[255,2534,522],{"class":949},[255,2536,1046],{"class":309},[255,2538,1745],{"class":302},[255,2540,522],{"class":949},[255,2542,1046],{"class":309},[255,2544,2545],{"class":949}," =>",[255,2547,959],{"class":302},[255,2549,2551,2554,2557,2559,2562,2564,2567,2569,2571,2574,2577,2579,2581,2584,2586,2589],{"class":257,"line":2550},28,[255,2552,2553],{"class":949},"          const",[255,2555,2556],{"class":1063}," map",[255,2558,522],{"class":949},[255,2560,2561],{"class":309}," Record",[255,2563,1753],{"class":302},[255,2565,2566],{"class":309},"string",[255,2568,327],{"class":302},[255,2570,2566],{"class":309},[255,2572,2573],{"class":949}," |",[255,2575,2576],{"class":309}," undefined",[255,2578,1759],{"class":302},[255,2580,1525],{"class":1239},[255,2582,2583],{"class":1063}," process",[255,2585,345],{"class":302},[255,2587,2588],{"class":1063},"env",[255,2590,1309],{"class":302},[255,2592,2594,2597,2599,2601,2603,2605,2607,2610],{"class":257,"line":2593},29,[255,2595,2596],{"class":1063},"          map",[255,2598,345],{"class":302},[255,2600,1484],{"class":1063},[255,2602,1240],{"class":1239},[255,2604,2316],{"class":1063},[255,2606,345],{"class":302},[255,2608,2609],{"class":1063},"url",[255,2611,1309],{"class":302},[255,2613,2615,2617,2619,2622,2624,2626,2628,2631],{"class":257,"line":2614},30,[255,2616,2596],{"class":1063},[255,2618,345],{"class":302},[255,2620,2621],{"class":1063},"DATABASE_HOSTNAME",[255,2623,1240],{"class":1239},[255,2625,2202],{"class":1063},[255,2627,345],{"class":302},[255,2629,2630],{"class":298},"getHost",[255,2632,1665],{"class":302},[255,2634,2636,2638,2640,2643,2645,2648,2650,2653,2655,2658],{"class":257,"line":2635},31,[255,2637,2596],{"class":1063},[255,2639,345],{"class":302},[255,2641,2642],{"class":1063},"DATABASE_PORT",[255,2644,1240],{"class":1239},[255,2646,2647],{"class":940}," `",[255,2649,1470],{"class":1469},[255,2651,2652],{"class":1063},"postgresContainer",[255,2654,345],{"class":1476},[255,2656,2657],{"class":298},"getMappedPort",[255,2659,1355],{"class":1476},[255,2661,2663,2666,2668,2670],{"class":257,"line":2662},32,[255,2664,2665],{"class":1063},"            databaseConfig",[255,2667,345],{"class":1476},[255,2669,2439],{"class":1063},[255,2671,1371],{"class":1476},[255,2673,2675,2678,2680,2682],{"class":257,"line":2674},33,[255,2676,2677],{"class":1476},"          )",[255,2679,1487],{"class":1469},[255,2681,1466],{"class":940},[255,2683,1309],{"class":302},[255,2685,2687,2689,2691,2694,2696,2698,2700,2702],{"class":257,"line":2686},34,[255,2688,2596],{"class":1063},[255,2690,345],{"class":302},[255,2692,2693],{"class":1063},"DATABASE_NAME",[255,2695,1240],{"class":1239},[255,2697,2236],{"class":1063},[255,2699,345],{"class":302},[255,2701,2415],{"class":1063},[255,2703,1309],{"class":302},[255,2705,2707,2709,2711,2714,2716,2718,2720,2722],{"class":257,"line":2706},35,[255,2708,2596],{"class":1063},[255,2710,345],{"class":302},[255,2712,2713],{"class":1063},"DATABASE_USERNAME",[255,2715,1240],{"class":1239},[255,2717,2236],{"class":1063},[255,2719,345],{"class":302},[255,2721,2384],{"class":1063},[255,2723,1309],{"class":302},[255,2725,2727,2729,2731,2734,2736,2738,2740,2742],{"class":257,"line":2726},36,[255,2728,2596],{"class":1063},[255,2730,345],{"class":302},[255,2732,2733],{"class":1063},"DATABASE_PASSWORD",[255,2735,1240],{"class":1239},[255,2737,2236],{"class":1063},[255,2739,345],{"class":302},[255,2741,2399],{"class":1063},[255,2743,1309],{"class":302},[255,2745,2747,2750,2752,2755,2757,2760,2763,2766],{"class":257,"line":2746},37,[255,2748,2749],{"class":949},"          return",[255,2751,2556],{"class":1063},[255,2753,2754],{"class":302},"[",[255,2756,2532],{"class":1063},[255,2758,2759],{"class":302},"] ",[255,2761,2762],{"class":949},"||",[255,2764,2765],{"class":940}," ''",[255,2767,1309],{"class":302},[255,2769,2771],{"class":257,"line":2770},38,[255,2772,2773],{"class":302},"        },\n",[255,2775,2777],{"class":257,"line":2776},39,[255,2778,2422],{"class":302},[255,2780,2782,2784,2787],{"class":257,"line":2781},40,[255,2783,2365],{"class":302},[255,2785,2786],{"class":298},"compile",[255,2788,1665],{"class":302},[255,2790,2792,2794,2796,2798,2800,2803],{"class":257,"line":2791},41,[255,2793,2023],{"class":1063},[255,2795,1240],{"class":1239},[255,2797,2457],{"class":1063},[255,2799,345],{"class":302},[255,2801,2802],{"class":298},"createNestApplication",[255,2804,1665],{"class":302},[255,2806,2808,2811,2813,2815,2817,2819,2821,2824,2826,2828],{"class":257,"line":2807},42,[255,2809,2810],{"class":1063},"    usersRepository",[255,2812,1240],{"class":1239},[255,2814,1771],{"class":1063},[255,2816,345],{"class":302},[255,2818,1461],{"class":298},[255,2820,937],{"class":302},[255,2822,2823],{"class":298},"getRepositoryToken",[255,2825,937],{"class":302},[255,2827,2226],{"class":1063},[255,2829,2830],{"class":302},"));\n",[255,2832,2834,2837,2839,2841,2844],{"class":257,"line":2833},43,[255,2835,2836],{"class":949},"    await",[255,2838,1771],{"class":1063},[255,2840,345],{"class":302},[255,2842,2843],{"class":298},"init",[255,2845,1665],{"class":302},[255,2847,2849],{"class":257,"line":2848},44,[255,2850,2851],{"class":302},"  });\n",[255,2853,2855,2858,2860,2862,2864,2866],{"class":257,"line":2854},45,[255,2856,2857],{"class":298},"  beforeEach",[255,2859,937],{"class":302},[255,2861,2303],{"class":949},[255,2863,1921],{"class":302},[255,2865,1735],{"class":949},[255,2867,959],{"class":302},[255,2869,2871,2873,2875,2877,2880],{"class":257,"line":2870},46,[255,2872,2836],{"class":949},[255,2874,2216],{"class":1063},[255,2876,345],{"class":302},[255,2878,2879],{"class":298},"delete",[255,2881,2882],{"class":302},"({});\n",[255,2884,2886],{"class":257,"line":2885},47,[255,2887,2851],{"class":302},[255,2889,2891,2894,2896,2898,2900,2902],{"class":257,"line":2890},48,[255,2892,2893],{"class":298},"  afterAll",[255,2895,937],{"class":302},[255,2897,2303],{"class":949},[255,2899,1921],{"class":302},[255,2901,1735],{"class":949},[255,2903,959],{"class":302},[255,2905,2907,2909,2911,2913,2915],{"class":257,"line":2906},49,[255,2908,2836],{"class":949},[255,2910,1771],{"class":1063},[255,2912,345],{"class":302},[255,2914,1930],{"class":298},[255,2916,1665],{"class":302},[255,2918,2920,2922],{"class":257,"line":2919},50,[255,2921,2329],{"class":298},[255,2923,1665],{"class":302},[255,2925,2927,2929,2931,2933,2936],{"class":257,"line":2926},51,[255,2928,2836],{"class":949},[255,2930,2202],{"class":1063},[255,2932,345],{"class":302},[255,2934,2935],{"class":298},"stop",[255,2937,1665],{"class":302},[255,2939,2941],{"class":257,"line":2940},52,[255,2942,2851],{"class":302},[11,2944,2945],{},"After the test setup, we can define our tests. In the first test suite, we test retrieving data from the database, but in the second and third test suites, we assert external service calls. We are trying to cover all possible scenarios of calling the external service, in our case, it can return either a successful response or HTTP 404 error.",[176,2947,2949],{"className":916,"code":2948,"language":918,"meta":185,"style":185},"describe('[GET] /users/:id', () => {\n  it('should return found user', async () => {\n    const userId = 'b618445a-0089-43d5-b9ca-e6f2fc29a11d';\n    const userDetails = {\n      id: userId,\n      firstName: 'tester',\n    };\n    const newUser = usersRepository.create(userDetails);\n    await usersRepository.save(newUser);\n    return request(app.getHttpServer())\n      .get(`/users/${userId}`)\n      .expect(HttpStatus.OK)\n      .then((response) => {\n        expect(response.body).toEqual(userDetails);\n      });\n  });\n  it('should return user roles from user auth service', async () => {\n    const userId = 'b618445a-0089-43d5-b9ca-e6f2fc29a11d';\n    return request(app.getHttpServer())\n      .get(`/users/${userId}/roles`)\n      .expect(HttpStatus.OK)\n      .then((response) => {\n        expect(response.body).toEqual(userRolesResponse);\n      });\n  });\n  it('should return 404 error when user roles were not found', async () => {\n    const userId = 'b618445a-0089-43d5-b9ca-e6f2fc29a11c'; // wrong user id\n    return request(app.getHttpServer()).get(`/users/${userId}/roles`).expect(HttpStatus.NOT_FOUND);\n  });\n});\n",[183,2950,2951,2966,2986,3000,3011,3022,3034,3039,3062,3080,3099,3120,3139,3156,3181,3186,3190,3209,3221,3237,3258,3274,3290,3313,3317,3321,3340,3356,3402,3406],{"__ignoreMap":185},[255,2952,2953,2955,2957,2960,2962,2964],{"class":257,"line":258},[255,2954,2153],{"class":298},[255,2956,937],{"class":302},[255,2958,2959],{"class":940},"'[GET] /users/:id'",[255,2961,1856],{"class":302},[255,2963,1735],{"class":949},[255,2965,959],{"class":302},[255,2967,2968,2971,2973,2976,2978,2980,2982,2984],{"class":257,"line":264},[255,2969,2970],{"class":298},"  it",[255,2972,937],{"class":302},[255,2974,2975],{"class":940},"'should return found user'",[255,2977,327],{"class":302},[255,2979,2303],{"class":949},[255,2981,1921],{"class":302},[255,2983,1735],{"class":949},[255,2985,959],{"class":302},[255,2987,2988,2990,2993,2995,2998],{"class":257,"line":270},[255,2989,1233],{"class":949},[255,2991,2992],{"class":1063}," userId",[255,2994,1240],{"class":1239},[255,2996,2997],{"class":940}," 'b618445a-0089-43d5-b9ca-e6f2fc29a11d'",[255,2999,1309],{"class":302},[255,3001,3002,3004,3007,3009],{"class":257,"line":276},[255,3003,1233],{"class":949},[255,3005,3006],{"class":1063}," userDetails",[255,3008,1240],{"class":1239},[255,3010,959],{"class":302},[255,3012,3013,3016,3018,3020],{"class":257,"line":443},[255,3014,3015],{"class":1063},"      id",[255,3017,522],{"class":1476},[255,3019,2992],{"class":1063},[255,3021,1371],{"class":302},[255,3023,3024,3027,3029,3032],{"class":257,"line":449},[255,3025,3026],{"class":1063},"      firstName",[255,3028,522],{"class":1476},[255,3030,3031],{"class":940}," 'tester'",[255,3033,1371],{"class":302},[255,3035,3036],{"class":257,"line":455},[255,3037,3038],{"class":302},"    };\n",[255,3040,3041,3043,3046,3048,3050,3052,3055,3057,3060],{"class":257,"line":461},[255,3042,1233],{"class":949},[255,3044,3045],{"class":1063}," newUser",[255,3047,1240],{"class":1239},[255,3049,2216],{"class":1063},[255,3051,345],{"class":302},[255,3053,3054],{"class":298},"create",[255,3056,937],{"class":302},[255,3058,3059],{"class":1063},"userDetails",[255,3061,1076],{"class":302},[255,3063,3064,3066,3068,3070,3073,3075,3078],{"class":257,"line":466},[255,3065,2836],{"class":949},[255,3067,2216],{"class":1063},[255,3069,345],{"class":302},[255,3071,3072],{"class":298},"save",[255,3074,937],{"class":302},[255,3076,3077],{"class":1063},"newUser",[255,3079,1076],{"class":302},[255,3081,3082,3084,3087,3089,3091,3093,3096],{"class":257,"line":472},[255,3083,1054],{"class":949},[255,3085,3086],{"class":298}," request",[255,3088,937],{"class":302},[255,3090,1810],{"class":1063},[255,3092,345],{"class":302},[255,3094,3095],{"class":298},"getHttpServer",[255,3097,3098],{"class":302},"())\n",[255,3100,3101,3103,3105,3107,3110,3112,3114,3116,3118],{"class":257,"line":1127},[255,3102,2365],{"class":302},[255,3104,1461],{"class":298},[255,3106,937],{"class":302},[255,3108,3109],{"class":940},"`/users/",[255,3111,1470],{"class":1469},[255,3113,1412],{"class":1063},[255,3115,1487],{"class":1469},[255,3117,1466],{"class":940},[255,3119,944],{"class":302},[255,3121,3122,3124,3127,3129,3132,3134,3137],{"class":257,"line":1150},[255,3123,2365],{"class":302},[255,3125,3126],{"class":298},"expect",[255,3128,937],{"class":302},[255,3130,3131],{"class":1063},"HttpStatus",[255,3133,345],{"class":302},[255,3135,3136],{"class":1063},"OK",[255,3138,944],{"class":302},[255,3140,3141,3143,3146,3148,3150,3152,3154],{"class":257,"line":1155},[255,3142,2365],{"class":302},[255,3144,3145],{"class":298},"then",[255,3147,1516],{"class":302},[255,3149,1597],{"class":972},[255,3151,1522],{"class":302},[255,3153,1735],{"class":949},[255,3155,959],{"class":302},[255,3157,3158,3161,3163,3165,3167,3170,3172,3175,3177,3179],{"class":257,"line":1455},[255,3159,3160],{"class":298},"        expect",[255,3162,937],{"class":302},[255,3164,1597],{"class":1063},[255,3166,345],{"class":302},[255,3168,3169],{"class":1063},"body",[255,3171,1902],{"class":302},[255,3173,3174],{"class":298},"toEqual",[255,3176,937],{"class":302},[255,3178,3059],{"class":1063},[255,3180,1076],{"class":302},[255,3182,3183],{"class":257,"line":1503},[255,3184,3185],{"class":302},"      });\n",[255,3187,3188],{"class":257,"line":1547},[255,3189,2851],{"class":302},[255,3191,3192,3194,3196,3199,3201,3203,3205,3207],{"class":257,"line":1553},[255,3193,2970],{"class":298},[255,3195,937],{"class":302},[255,3197,3198],{"class":940},"'should return user roles from user auth service'",[255,3200,327],{"class":302},[255,3202,2303],{"class":949},[255,3204,1921],{"class":302},[255,3206,1735],{"class":949},[255,3208,959],{"class":302},[255,3210,3211,3213,3215,3217,3219],{"class":257,"line":1558},[255,3212,1233],{"class":949},[255,3214,2992],{"class":1063},[255,3216,1240],{"class":1239},[255,3218,2997],{"class":940},[255,3220,1309],{"class":302},[255,3222,3223,3225,3227,3229,3231,3233,3235],{"class":257,"line":1568},[255,3224,1054],{"class":949},[255,3226,3086],{"class":298},[255,3228,937],{"class":302},[255,3230,1810],{"class":1063},[255,3232,345],{"class":302},[255,3234,3095],{"class":298},[255,3236,3098],{"class":302},[255,3238,3239,3241,3243,3245,3247,3249,3251,3253,3256],{"class":257,"line":1584},[255,3240,2365],{"class":302},[255,3242,1461],{"class":298},[255,3244,937],{"class":302},[255,3246,3109],{"class":940},[255,3248,1470],{"class":1469},[255,3250,1412],{"class":1063},[255,3252,1487],{"class":1469},[255,3254,3255],{"class":940},"/roles`",[255,3257,944],{"class":302},[255,3259,3260,3262,3264,3266,3268,3270,3272],{"class":257,"line":1607},[255,3261,2365],{"class":302},[255,3263,3126],{"class":298},[255,3265,937],{"class":302},[255,3267,3131],{"class":1063},[255,3269,345],{"class":302},[255,3271,3136],{"class":1063},[255,3273,944],{"class":302},[255,3275,3276,3278,3280,3282,3284,3286,3288],{"class":257,"line":1644},[255,3277,2365],{"class":302},[255,3279,3145],{"class":298},[255,3281,1516],{"class":302},[255,3283,1597],{"class":972},[255,3285,1522],{"class":302},[255,3287,1735],{"class":949},[255,3289,959],{"class":302},[255,3291,3292,3294,3296,3298,3300,3302,3304,3306,3308,3311],{"class":257,"line":1650},[255,3293,3160],{"class":298},[255,3295,937],{"class":302},[255,3297,1597],{"class":1063},[255,3299,345],{"class":302},[255,3301,3169],{"class":1063},[255,3303,1902],{"class":302},[255,3305,3174],{"class":298},[255,3307,937],{"class":302},[255,3309,3310],{"class":1063},"userRolesResponse",[255,3312,1076],{"class":302},[255,3314,3315],{"class":257,"line":1655},[255,3316,3185],{"class":302},[255,3318,3319],{"class":257,"line":1668},[255,3320,2851],{"class":302},[255,3322,3323,3325,3327,3330,3332,3334,3336,3338],{"class":257,"line":1673},[255,3324,2970],{"class":298},[255,3326,937],{"class":302},[255,3328,3329],{"class":940},"'should return 404 error when user roles were not found'",[255,3331,327],{"class":302},[255,3333,2303],{"class":949},[255,3335,1921],{"class":302},[255,3337,1735],{"class":949},[255,3339,959],{"class":302},[255,3341,3342,3344,3346,3348,3351,3353],{"class":257,"line":1678},[255,3343,1233],{"class":949},[255,3345,2992],{"class":1063},[255,3347,1240],{"class":1239},[255,3349,3350],{"class":940}," 'b618445a-0089-43d5-b9ca-e6f2fc29a11c'",[255,3352,1534],{"class":302},[255,3354,3355],{"class":925},"// wrong user id\n",[255,3357,3358,3360,3362,3364,3366,3368,3370,3373,3375,3377,3379,3381,3383,3385,3387,3389,3391,3393,3395,3397,3400],{"class":257,"line":2550},[255,3359,1054],{"class":949},[255,3361,3086],{"class":298},[255,3363,937],{"class":302},[255,3365,1810],{"class":1063},[255,3367,345],{"class":302},[255,3369,3095],{"class":298},[255,3371,3372],{"class":302},"()).",[255,3374,1461],{"class":298},[255,3376,937],{"class":302},[255,3378,3109],{"class":940},[255,3380,1470],{"class":1469},[255,3382,1412],{"class":1063},[255,3384,1487],{"class":1469},[255,3386,3255],{"class":940},[255,3388,1902],{"class":302},[255,3390,3126],{"class":298},[255,3392,937],{"class":302},[255,3394,3131],{"class":1063},[255,3396,345],{"class":302},[255,3398,3399],{"class":1063},"NOT_FOUND",[255,3401,1076],{"class":302},[255,3403,3404],{"class":257,"line":2593},[255,3405,2851],{"class":302},[255,3407,3408],{"class":257,"line":2614},[255,3409,3410],{"class":302},"});\n",[11,3412,3413,3414,345],{},"The complete example you can find ",[3415,3416,3420],"a",{"href":3417,"rel":3418},"https://github.com/kochax/integration-testing-nestjs",[3419],"nofollow","here",[30,3422,3424],{"id":3423},"should-you-avoid-integration-tests",[15,3425,3426],{},"Should You Avoid Integration Tests?",[11,3428,3429],{},"Despite the disadvantages just outlined, for many users, integration tests can still be manageable with a small number of microservices, and in these situations, they still",[11,3431,3432],{},"make a lot of sense. But what happens with 3, 4, 10, or 20 services? Very quickly these test suites become hugely bloated, and in the worst case, they can result in a Cartesian-like explosion in the scenarios under test.",[11,3434,3435],{},"In fact, even with a small number of microservices, these tests become difficult when you have multiple teams sharing integration tests. With a shared integration test suite, you undermine your goal of independent deployability. Your ability as a team to deploy a microservice now requires that a test suite shared by multiple teams passes.",[11,3437,3438,3439,3444,3445,3449],{},"What is one of the key problems we are trying to address when we use the integration tests outlined previously? We are trying to ensure that when we deploy a new service to production, our changes won’t break consumers. However, schemas can’t pick up semantic breakages, namely changes in behavior that cause breakages due to backward incompatibility. Integration tests absolutely can help catch these semantic breakages, but they do so at a great cost. Ideally, we’d want to have some type of test that can pick up semantic breaking changes and run in a reduced scope, improving test isolation (and therefore speed of feedback). This is where ",[3415,3440,3443],{"href":3441,"rel":3442},"https://thoughtworks.github.io/pacto/patterns/cdc/",[3419],"contract tests"," and ",[3415,3446,3448],{"href":3441,"rel":3447},[3419],"consumer-driven"," contracts come in.",[30,3451,3453],{"id":3452},"conclusion",[15,3454,3455],{},"Conclusion",[11,3457,3458],{},"In conclusion, NodeJS integration testing is a critical aspect of software development that helps developers to ensure the quality and reliability of their applications. By identifying and resolving issues early in the development process, developers can deliver more stable and performant applications to their users, resulting in a better user experience and improved business outcomes.",[11,3460,3461],{},"Integration testing is crucial for ensuring the quality and reliability of NodeJS applications. It helps to identify issues that may arise when different components are integrated, such as incorrect data flow or communication problems between services. By identifying and resolving these issues early in the development process, developers can avoid costly and time-consuming errors in the later stages of the application’s lifecycle.",[11,3463,3464],{},"Additionally, integration testing is important for maintaining the stability and performance of the application over time. Regular testing can help to detect issues that may arise as a result of updates or changes to external dependencies, ensuring that the application continues to function as expected.",[792,3466,3467],{},"html pre.shiki code .sjThu, html code.shiki .sjThu{--shiki-default:#848BBD;--shiki-default-font-style:italic}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sgRlq, html code.shiki .sgRlq{--shiki-default:#36F9F6}html pre.shiki code .sFl1-, html code.shiki .sFl1-{--shiki-default:#FF8B39}html pre.shiki code .scrqX, html code.shiki .scrqX{--shiki-default:#FEDE5D}html pre.shiki code .sRQ1T, html code.shiki .sRQ1T{--shiki-default:#FE4450}html pre.shiki code .sH7Js, html code.shiki .sH7Js{--shiki-default:#FF7EDB;--shiki-default-font-style:italic}html pre.shiki code .swVBR, html code.shiki .swVBR{--shiki-default:#FE4450;--shiki-default-font-weight:bold}html pre.shiki code .sqjNi, html code.shiki .sqjNi{--shiki-default:#FF7EDB}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sW278, html code.shiki .sW278{--shiki-default:#FFFFFFEE}html pre.shiki code .sfE7D, html code.shiki .sfE7D{--shiki-default:#72F1B8}html pre.shiki code .s2hdG, html code.shiki .s2hdG{--shiki-default:#B6B1B1}html pre.shiki code .sSmKr, html code.shiki .sSmKr{--shiki-default:#F97E72}",{"title":185,"searchDepth":443,"depth":443,"links":3469},[],[3471],"NodeJS","2023-03-22",[857,858,859,860,861,862,863,864],{"enable_toc":825},"/blog/integration-testing-with-nodejs",{"title":878,"description":855},"blog/integration-testing-with-nodejs",[870,837,871,872,873,874],"lQ2Cv9J0YQQowM_rW1i2QatMYuK1ctApWOMSbdncwXI",{"id":3481,"title":3482,"body":3483,"categories":3490,"cover":853,"date":3491,"description":855,"extension":812,"keywords":3492,"meta":3493,"navigation":825,"path":3494,"published":827,"seo":3495,"stem":3496,"tags":3497,"__hash__":3499},"blog/blog/test.md","Test blog post",{"type":8,"value":3484,"toc":3488},[3485],[11,3486,3487],{},"This is how to install node-rdkafka older versions on M1 Apple test",{"title":185,"searchDepth":443,"depth":443,"links":3489},[],[852],"2023-07-30",[857,858,859,860,861,862,863,864],{"enable_toc":825},"/blog/test",{"title":3482,"description":855},"blog/test",[3498],"someother","zYIHDqFcU9f8UJx8Zsjpe4xuIsCfH_rL_bte4U3nkk4",{"id":3501,"title":3502,"body":3503,"categories":3509,"cover":853,"date":3510,"description":855,"extension":812,"keywords":3511,"meta":3512,"navigation":825,"path":3513,"published":827,"seo":3514,"stem":3515,"tags":3516,"__hash__":3517},"blog/blog/test2.md","Test blog post 2",{"type":8,"value":3504,"toc":3507},[3505],[11,3506,848],{},{"title":185,"searchDepth":443,"depth":443,"links":3508},[],[852],"2023-03-30",[857,858,859,860,861,862,863,864],{"enable_toc":825},"/blog/test2",{"title":3502,"description":855},"blog/test2",[3498],"mRGwo5F3URB43dAReVASFXZ3anMkaQNETFX2i-BH2dc",1792785208247]