Skip to content

achieve 100% test coverage in expressjs - #7112

Open
bhavya3024 wants to merge 1 commit into
expressjs:masterfrom
bhavya3024:master
Open

achieve 100% test coverage in expressjs#7112
bhavya3024 wants to merge 1 commit into
expressjs:masterfrom
bhavya3024:master

Conversation

@bhavya3024

@bhavya3024bhavya3024 commented Mar 18, 2026

Copy link
Copy Markdown
Contributor
image

Test Coverage Mapping - Comprehensive Reference

Quick Summary Table

#Test NameTest FileLines CoveredType
1Callback not invoked twiceapp.listen.js604Branch (guard)
2Array argument not flattenedreq.is.js273Statement
3Non-string JSON bodyres.jsonp.js291Branch
4Array append to single valueres.append.js636Branch (ternary)
5aMissing URL argumentres.redirect.js824Branch (deprecate)
5bNon-string URLres.redirect.js828Branch (deprecate)
5cNon-number statusres.redirect.js832Branch (deprecate)
6Missing options object (unit)view.js53Statement (default)
6a-6dView engine integration testsview.js56-62, 75-81Branches (error handling)
6eValid engine renderview.js75 (TRUE)Branch (engine load)
6fCached engine reuseview.js75 (FALSE)Branch (cache hit)
7a-7fSendfile event guardsres.sendFile.guards.js927-967, 976Statements/Branches

Detailed Test Cases


Test 1: Callback Guard in app.listen()

Location:test/app.listen.js

Test Syntax

it('should not invoke callback more than once',function(done){varapp=express()varcount=0varserver=app.listen(0,function(){count++server.emit('error',newError('boom'))setImmediate(function(){assert.strictEqual(count,1)// Callback was called exactly onceserver.close(done)})})})

Covered Code

File:lib/application.js (lines 603-607)

vardone=args[args.length-1]=function(){if(called)return;// ← LINE 604: GUARD CONDITION (prevents re-invocation)called=true;callback.apply(this,arguments);};

What This Covers

  • Line 604: The guard condition if (called) return;
  • How: Test emits an error event after the initial callback fires. The guard prevents the callback from being re-invoked when the server error event fires again.
  • Why: Ensures callbacks are only invoked once, preventing duplicate executions in error scenarios.

Test 2: Array Argument Handling in req.is()

Location:test/req.is.js

Test Syntax

it('should not flatten the argument',function(done){varapp=express()app.use(function(req,res){res.json(req.is(['text/*','application/json']))// Pass array directly})request(app).post('/').type('application/json').send('{}').expect(200,'"application/json"',done)})

Covered Code

File:lib/request.js (lines 269-281)

req.is=functionis(types){vararr=types;if(!Array.isArray(types)){// ← LINE 273: When FALSE, skip flattening blockarr=newArray(arguments.length);for(vari=0;i<arr.length;i++){arr[i]=arguments[i];}}returntypeis(this,arr);};

What This Covers

  • Line 273: The condition if (!Array.isArray(types)) evaluates to FALSE
  • How: Test passes a pre-formed array to req.is(), causing the condition to be false and skipping the flattening logic.
  • Why: Normal usage passes string arguments that need flattening. This test covers the case where an array is passed directly, which should skip flattening.

Test 3: Non-String JSON Body in res.jsonp()

Location:test/res.jsonp.js

Test Syntax

it('should handle non-string stringify return values',function(done){varapp=express()app.use(function(req,res){varoriginal=JSON.stringifyJSON.stringify=function(){return42// Force non-string return (normally impossible)}try{res.jsonp('ignored')}finally{JSON.stringify=original}})request(app).get('/?callback=cb').expect('Content-Type','text/javascript; charset=utf-8').expect(200,/cb\(42\)/,done)})

Covered Code

File:lib/response.js (lines 288-296)

if(body===undefined){body=''}elseif(typeofbody==='string'){// ← LINE 291: When FALSE, skip escape logic// replace chars not allowed in JavaScript that are in JSONbody=body.replace(/\u2028/g,'\\u2028').replace(/\u2029/g,'\\u2029')}

What This Covers

  • Line 291: The condition else if (typeof body === 'string') evaluates to FALSE
  • How: By mocking JSON.stringify() to return a number (42) instead of a string, we force the else-if condition to be false, skipping the Unicode escape logic.
  • Why: Normal JSON.stringify() always returns a string, so this branch is unreachable in production. However, this defensive code handles unexpected returns if assumptions change. Testing it verifies the guard works.

Test 4: Array Append to Single Header Value

Location:test/res.append.js

Test Syntax

it('should append an array to an existing single value',function(done){varapp=express()app.use(function(req,res){res.set('Set-Cookie','foo=bar')// Single valueres.append('Set-Cookie',['fizz=buzz','pet=tobi'])// Append arrayres.end()})request(app).get('/').expect(200).expect(shouldHaveHeaderValues('Set-Cookie',['foo=bar','fizz=buzz','pet=tobi'])).end(done)})

Covered Code

File:lib/response.js (lines 633-640)

if(prev){value=Array.isArray(prev) ? prev.concat(val)
: Array.isArray(val) ? [prev].concat(val)// ← LINE 636: EXECUTES (prev is single, val is array)
: [prev,val]}returnthis.set(field,value);

What This Covers

  • Line 636: The ternary branch [prev].concat(val)
  • How: Set a single header value, then append an array. The condition Array.isArray(val) is true, executing the middle ternary branch.
  • Why: The append method handles three scenarios: (1) both array, (2) prev single + val array, (3) both single. This test covers scenario 2 by converting the single value to an array and concatenating.

Test 5a: Missing URL Argument in res.redirect()

Location:test/res.redirect.js

Test Syntax

it('should handle missing url argument',function(done){varapp=express()app.use(function(req,res){res.redirect('')// Empty string URL})request(app).get('/').expect(302).expect('Location','').end(done)})

Covered Code

File:lib/response.js (lines 823-825)

if(!address){// ← LINE 824: DEPRECATION GUARD (address is empty string)deprecate('Provide a url argument');}

What This Covers

  • Line 824: The deprecation warning for missing URL
  • How: Pass empty string to res.redirect(), triggering the if (!address) condition.
  • Why: Validates defensive check that warns when URL argument is missing or falsy.

Test 5b: Non-String URL in res.redirect()

Location:test/res.redirect.js

Test Syntax

it('should coerce when url is not a string',function(done){varapp=express()app.use(function(req,res){res.redirect(42)// Number instead of string})request(app).get('/').expect(302).expect('Location','42').end(done)})

Covered Code

File:lib/response.js (lines 827-829)

if(typeofaddress!=='string'){// ← LINE 828: DEPRECATION GUARD (address is number)deprecate('Url must be a string');}

What This Covers

  • Line 828: The deprecation warning for non-string URL
  • How: Pass a number (42) to res.redirect(), triggering the typeof check.
  • Why: Validates defensive check that warns when URL is not a string type.

Test 5c: Non-Number Status in res.redirect()

Location:test/res.redirect.js

Test Syntax

it('should fail when status is not a number',function(done){varapp=express()app.use(function(req,res){res.redirect('302','/somewhere')// String status instead of number})request(app).get('/').expect(500,done)// Causes error})

Covered Code

File:lib/response.js (lines 831-833)

if(typeofstatus!=='number'){// ← LINE 832: DEPRECATION GUARD (status is string)deprecate('Status must be a number');}

What This Covers

  • Line 832: The deprecation warning for non-number status
  • How: Pass a string ('302') as status instead of number, triggering the typeof check.
  • Why: Validates defensive check that warns when status code is not numeric.

Test 6: Missing Options in View Constructor

Location:test/view.js

Original Unit Test

it('should handle missing options object',function(){assert.throws(function(){newView('name')// No options parameter},/Nodefaultenginewasspecifiedandnoextensionwasprovided\./)})

Integration Tests Added

// Test 6a: Missing engine/extension via app.render()it('should throw error via app.render() without view engine or extension',function(done){varapp=express()try{app.render('user',function(err){done(newError('Should have thrown'))})done(newError('Should have thrown'))}catch(err){assert.match(err.message,/Nodefaultenginewasspecifiedandnoextensionwasprovided\./)done()}})// Test 6b: Missing engine/extension via res.render()it('should throw error via res.render() without view engine or extension',function(done){varapp=express()app.use(function(req,res){res.render('user')})request(app).get('/').expect(500,/Nodefaultenginewasspecifiedandnoextensionwasprovided\./,done)})// Test 6c: Non-existent view engine via app.render()it('should throw error for non-existent view engine',function(done){varapp=express()app.set('view engine','unknown')app.set('views',path.join(__dirname,'fixtures'))try{app.render('user',function(err){done(newError('Should have thrown'))})done(newError('Should have thrown'))}catch(err){assert.match(err.message,/Cannotfindmodule/)done()}})// Test 6d: Non-existent view engine via res.render()it('should throw error via res.render() for non-existent view engine',function(done){varapp=express()app.set('view engine','unknown')app.set('views',path.join(__dirname,'fixtures'))app.use(function(req,res){res.render('user')})request(app).get('/').expect(500,/Cannotfindmodule/,done)})// Test 6e: Valid engine renders successfullyit('should successfully render via res.render() with valid engine',function(done){varapp=express()app.engine('tmpl',tmpl)app.set('view engine','tmpl')app.set('views',path.join(__dirname,'fixtures'))app.locals.user={name: 'tobi'}app.use(function(req,res){res.render('user')})request(app).get('/').expect('<p>tobi</p>',done)})// Test 6f: Cached engine reuse (LINE 53 FALSE BRANCH)it('should reuse cached engine on second render',function(done){varapp=express()app.engine('tmpl',tmpl)app.set('view engine','tmpl')app.set('views',path.join(__dirname,'fixtures'))app.locals.user={name: 'loki'}varrenderCount=0app.use(function(req,res){if(renderCount===0){renderCount++res.render('user',function(err,html){if(err)returnres.send(500)// render again to test cached engine (exercises line 53 FALSE branch)res.render('user',function(err,html2){if(err)returnres.send(500)res.send(html2)})})}})request(app).get('/').expect('<p>loki</p>',done)})

Covered Code

File:lib/view.js (lines 52-86)

functionView(name,options){varopts=options||{};// ← LINE 53: DEFAULT ASSIGNMENT (options is undefined)this.defaultEngine=opts.defaultEngine;this.ext=extname(name);this.name=name;this.root=opts.root;if(!this.ext&&!this.defaultEngine){thrownewError('No default engine was specified and no extension was provided.');}varfileName=name;if(!this.ext){// get extension from default engine namethis.ext=this.defaultEngine[0]!=='.'
? '.'+this.defaultEngine
: this.defaultEngine;fileName+=this.ext;}if(!opts.engines[this.ext]){// ← LINE 75: BRANCH (TRUE: load engine, FALSE: use cached)// load enginevarmod=this.ext.slice(1)debug('require "%s"',mod)// default engine exportvarfn=require(mod).__expressif(typeoffn!=='function'){thrownewError('Module "'+mod+'" does not provide a view engine.')}opts.engines[this.ext]=fn}// store loaded enginethis.engine=opts.engines[this.ext];// lookup paththis.path=this.lookup(fileName);}

Coverage Analysis

TestCoverageDetails
6 (unit)Line 53 (TRUE)Exercises default assignment when options is undefined
6aLines 56-62Tests missing engine throws expected error via app.render()
6bLines 56-62Tests missing engine throws expected error via res.render() HTTP 500
6cLines 77-81Tests non-existent engine throws error via app.render()
6dLines 77-81Tests non-existent engine throws error via res.render() HTTP 500
6eLines 75-86Tests successful render with valid engine loaded (line 75 TRUE branch)
6fLine 75 (FALSE)Cached engine reuse - exercises the FALSE branch when engine already in cache

What These Covers

  • Line 53: The default assignment opts = options || {}
  • Lines 56-62: Error condition when no default engine and no file extension
  • Line 75: Both branches - TRUE: loads engine from module, FALSE: reuses cached engine
  • Lines 77-81: Engine validation after require (ensures __express is a function)
  • How: Tests cover the full View lifecycle through Express API (app.render and res.render)
  • Why: Verifies View constructor correctly handles missing options, engine loading, caching, and error scenarios. Integration tests ensure the View class works correctly when invoked through the HTTP API, not just direct instantiation.

Branch Coverage: 100%

  • Line 53 (DEFAULT): ✓ Tested by unit test
  • Line 75 (ENGINE CACHE): ✓ Tested by 6e (first render, TRUE) and 6f (second render, FALSE)
  • Line 60-61 (ERROR CONDITION): ✓ Tested by 6a and 6b

Tests 7a-7f: Sendfile Event Guard Conditions

Location:test/res.sendFile.guards.js

Test 7a Syntax

it('should ignore ECONNRESET finish after already ended',function(done){runScenario({pipe: function(file){file.emit('end')// End event completes stream first},onFinished: function(file,onfinish){setImmediate(function(){onfinish({code: 'ECONNRESET'})// Then error arrives})},assert: function(count,err){assert.strictEqual(count,1)// Callback invoked only onceassert.strictEqual(err,null)// No error recorded}},done)})

Test 7b Syntax

it('should ignore directory event after error',function(done){runScenario({pipe: function(file){file.emit('error',newError('boom'))// Error firstfile.emit('directory')// Directory event after},onFinished: function(){},assert: function(count,err){assert.strictEqual(count,1)assert.ok(err)}},done)})

Test 7c Syntax

it('should ignore error event after end',function(done){runScenario({pipe: function(file){file.emit('end')// End completes firstfile.emit('error',newError('late'))// Error after},onFinished: function(){},assert: function(count,err){assert.strictEqual(count,1)assert.strictEqual(err,null)}},done)})

Test 7d Syntax

it('should ignore end event after error',function(done){runScenario({pipe: function(file){file.emit('error',newError('boom'))// Error firstfile.emit('end')// End after},onFinished: function(){},assert: function(count,err){assert.strictEqual(count,1)assert.ok(err)}},done)})

Test 7e Syntax

it('should handle non-ECONNRESET finish errors via onerror',function(done){runScenario({pipe: function(){},onFinished: function(file,onfinish){onfinish({code: 'EPIPE'})// Different error type},assert: function(count,err){assert.strictEqual(count,1)assert.ok(err)assert.strictEqual(err.code,'EPIPE')}},done)})

Test 7f Syntax

it('should skip finish completion when done before setImmediate',function(done){runScenario({pipe: function(file){file.emit('end')// Complete before setImmediate},onFinished: function(file,onfinish){onfinish()// Call finish callback immediately},assert: function(count,err){assert.strictEqual(count,1)assert.strictEqual(err,null)}},done)})

Covered Code

File:lib/response.js (lines 920-979) - Internal sendfile function

functionsendfile(res,file,options,callback){vardone=false;varstreaming;// request abortedfunctiononaborted(){if(done)return;// ← LINE 927: GUARD (prevents re-execution)done=true;varerr=newError('Request aborted');err.code='ECONNABORTED';callback(err);}// directoryfunctionondirectory(){if(done)return;// ← LINE 937: GUARD (prevents re-execution)done=true;varerr=newError('EISDIR, read');err.code='EISDIR';callback(err);}// errorsfunctiononerror(err){if(done)return;// ← LINE 947: GUARD (prevents re-execution)done=true;callback(err);}// endedfunctiononend(){if(done)return;// ← LINE 954: GUARD (prevents re-execution)done=true;callback();}// finishedfunctiononfinish(err){if(err&&err.code==='ECONNRESET')returnonaborted();// ← LINE 966-967if(err)returnonerror(err);if(done)return;setImmediate(function(){if(streaming!==false&&!done){onaborted();return;}if(done)return;// ← LINE 976: GUARD (final check before callback)done=true;callback();});}// wire eventsfile.on('directory',ondirectory);file.on('end',onend);file.on('error',onerror);file.on('file',onfile);file.on('stream',onstream);onFinished(res,onfinish);// pipe the filefile.pipe(res);}

What This Covers

TestLinesGuard LogicCoverage
7a927onaborted() guard prevents re-execution when already doneSimulates end→ECONNRESET order
7b937ondirectory() guard prevents re-execution when already doneSimulates error→directory order
7c947onerror() guard prevents re-execution when already doneSimulates end→error order
7d954onend() guard prevents re-execution when already doneSimulates error→end order
7e966-967Error condition checks for non-ECONNRESET errorsValidates EPIPE and other errors handled
7f976Final completion guard before callbackEnsures setImmediate completion guard works

Why This Matters

The sendfile() function uses multiple event handlers (onaborted, ondirectory, onerror, onend, onfinish) and a done flag to ensure the callback is invoked exactly once. Each handler has a guard if (done) return; that prevents re-execution. These tests simulate different event orderings to verify all guards work correctly and prevent duplicate callbacks in race conditions.


Summary Statistics

MetricCount
Total Test Cases13
Test Files Created/Modified7
Original Uncovered Lines7 distinct lines
Original Uncovered Branches10 distinct branches
Lines Now Covered100%
Branches Now Covered100%
All Tests Passing1269 ✅

Coverage Achieved

  • application.js: 100% (Line 604 covered)
  • request.js: 100% (Line 273 covered)
  • response.js: 100% (Lines 291, 636, 824, 828, 832, 927-967, 976 covered)
  • view.js: 100% (Line 53 covered)
  • All other files: 100%

Overall Coverage: 100% Statements | 100% Branches | 100% Functions | 100% Lines

@krzysdz

Copy link
Copy Markdown
Contributor

First quick look - 5a-c do not really check if the deprecation is shown (same as #7105 (comment)). The new tests for some reason to cover these lines check for the results of implicit conversions (current behaviour) instead of results of freshly covered code.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bhavya3024@krzysdz