I've been struggling with this for the past week, so I finally need to admit that I don't know why this is happening.
I'm using supertest with express and TypeScript to test the implementation of some of my API routes. Keep in mind, I'm actively opening a db connection and writing to the DB here, and not using mocks for these methods.
However, I have a dependency in some of my controllers that rely on a logger instance attached to each controller (which is just an ES6 class).
I'm trying to figure out why it's not mocking it properly with the code below:
utils/logger.ts
:
class Logger {
private filename: string
public constructor(filename: string) {
this.filename = filename
}
public debug(message: string) {
console.log('debug:', message)
}
}
controllers/user.ts
class UserController {
public logger: any
constructor() {
this.logger = new Logger('UserController.ts')
}
async create(req: Request, res: Response) {
this.logger.debug('creating') <-- THIS KEEPS ERRORING with: TypeError: Cannot read property 'logger' of undefined
return res.status(200).end()
}
}
export default new UserController()
server/routes/user.ts
:
import users from '../controllers/user'
class UserRoutes {
constructor(server: any) {
server.post('/api/v1/users/create', users.create)
}
}
export default UserRoutes
here is the app server I'm passing to supertest
:
server.ts
:
import bodyParser from 'body-parser'
import express from 'express'
import database from '../server/db'
import Routes from '../server/routes'
class TestApp {
public server: any
public database: any
constructor() {
this.initDatabase()
this.run()
}
public async run(): Promise<void> {
this.server = express()
this.middleware()
this.routes()
}
private middleware(): void {
this.server.use(bodyParser.json())
}
private routes(): void {
new Routes(this.server)
}
public initDatabase(): void {
this.database = new database()
}
public closeDatabase(): void {
this.database.close()
}
}
export default TestApp
And finally the actual test:
import express, { Application } from 'express'
import request from 'supertest'
import TestApp from './server'
let server: any
const initServer = () => {
jest.mock('../server/controllers/user', () => {
return jest.fn().mockImplementation(() => {
return {
logger: {
debug: jest.fn()
}
}
})
})
const testApp = new TestApp()
const server: Application = testApp.server
const exp = express()
return exp.use(server)
}
beforeAll(async () => {
server = initServer()
})
describe('POST /api/v1/users', () => {
test('should create a new user', async () => {
const res = await request(server)
.post('/api/v1/users/create')
.send({
username: 'testing'
})
expect(res.status).toEqual(200)
})
})
I've tried mocking both the instance of logger
in the user controller AS WELL AS the actual logger.ts
entire class, but nothing has been working so far. Here's the full stack trace:
TypeError: Cannot read property 'logger' of undefined
46 |
> 47 | this.logger.debug('creating user')
| ^
48 |
49 | try {
50 | const user = await User.create({
at src/server/controllers/user.ts:47:10
at src/server/controllers/user.ts:8:71
at __awaiter (src/server/controllers/user.ts:4:12)
at create (src/server/controllers/user.ts:50:16)
at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
at next (node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
at node_modules/express/lib/router/index.js:281:22
at Function.process_params (node_modules/express/lib/router/index.js:335:12)
at next (node_modules/express/lib/router/index.js:275:10)
at SessionStrategy.strategy.pass (node_modules/passport/lib/middleware/authenticate.js:343:9)
at SessionStrategy.authenticate (node_modules/passport/lib/strategies/session.js:75:10)
at attempt (node_modules/passport/lib/middleware/authenticate.js:366:16)
at authenticate (node_modules/passport/lib/middleware/authenticate.js:367:7)
at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
at trim_prefix (node_modules/express/lib/router/index.js:317:13)
at node_modules/express/lib/router/index.js:284:7
at Function.process_params (node_modules/express/lib/router/index.js:335:12)
at next (node_modules/express/lib/router/index.js:275:10)
console.log src/server/controllers/user.ts:46
Why is this
(the user controller instance) always undefined
?
Here you lose the context:
server/routes/user.ts
server.post('/api/v1/users/create', users.create)
There are a few options to avoid it
controllers/user.ts
class UserController {
// ...
create = async (req: Request, res: Response) => {
this.logger.debug('creating');
return res.status(200).end();
}
// ...
}
server/routes/user.ts
class UserRoutes {
constructor(server: any) {
server.post('/api/v1/users/create', users.create.bind(users));
}
}
bind
server/routes/user.ts
class UserRoutes {
constructor(server: any) {
server.post('/api/v1/users/create', (req, res) => users.create(req, res));
}
}