Unit Testing in a Coffee House

Creating Modular Mocks for the Obsidian API in Jest

Introduction

Testing is crucial in software development, especially for the several custom plugins I’m building for Obsidian. I rely on Jest, a testing tool that incorporates Mocks, to simulate Obsidian’s API. This approach is key to making tests isolated and dependable. In this post, I’ll show you how to create a flexible and scalable mock framework for the Obsidian API using Jest, tailored for developing multiple custom plugins.

Why Modular Mocks?

As your plugin grows, so does the complexity of your testing environment. A modular approach to mocking allows you to:

  • Maintain Separation of Concerns: Each mock file focuses on a specific aspect of the API, making it easier to manage and update.
  • Improve Scalability: Adding new mocks or updating existing ones is straightforward, even as your project expands.
  • Ensure Consistency: A centralized index.js file aggregates all your mocks, providing a single source of truth for your tests.

Setting Up Your Project Structure

Start by organizing your project directory to accommodate the modular mocks:

project-root/
├── __mocks__/
│   ├── App.js
│   ├── TFile.js
│   ├── Modal.js
│   ├── Plugin.js
│   ├── Vault.js
│   ├── Workspace.js
│   ├── MarkdownView.js
│   ├── Setting.js
│   ├── StatusBarItem.js
│   └── index.js
├── src/
├── tests/
└── package.json

Creating the Individual Mock Files

Each file in the __mocks__ directory represents a component of the Obsidian API. Here’s a brief look at how to structure these files:

  • __mocks__/TFile.js: Represents files within Obsidian’s vault.

    class TFile {
      constructor(path) {
        this.path = path;
      }
    }
    module.exports = TFile;
  • __mocks__/Modal.js: Used for creating custom modals.

    class Modal {
      open() {}
      close() {}
      contentEl = {
        createEl: jest.fn(),
        empty: jest.fn(),
      };
    }
    module.exports = Modal;
  • __mocks__/Plugin.js: Represents the core plugin functionality.

    class Plugin {
      addSettingTab() {}
      registerEvent() {}
      addStatusBarItem() {}
      loadData() {
        return Promise.resolve(null);
      }
      saveData() {
        return Promise.resolve();
      }
    }
    module.exports = Plugin;

Centralizing Mocks with index.js

To make your mocks easily accessible, create an index.js file in the __mocks__ directory that consolidates all individual mocks:

const App = require('./App');
const TFile = require('./TFile');
const Modal = require('./Modal');
const Plugin = require('./Plugin');
const Vault = require('./Vault');
const Workspace = require('./Workspace');
const MarkdownView = require('./MarkdownView');
const Setting = require('./Setting');
const StatusBarItem = require('./StatusBarItem');

module.exports = {
  App,
  TFile,
  Modal,
  Plugin,
  Vault,
  Workspace,
  MarkdownView,
  Setting,
  StatusBarItem,
};

Using the Mocks in Your Tests

With the mocks in place, you can import them directly in your test files. For example:

import { Plugin, TFile, Modal } from '../__mocks__/index.js';
import ParaTaxonomyClassifierPlugin from '../src/main';

describe('ParaTaxonomyClassifierPlugin', () => {
    let plugin;

    beforeEach(() => {
        plugin = new ParaTaxonomyClassifierPlugin();
        plugin.settings = DEFAULT_SETTINGS;
    });

    it('should do something with TFile and Modal', () => {
        const mockFile = new TFile('path/to/file.md');
        const modal = new Modal();
        plugin.handleFileSave(mockFile);
        expect(modal.open).toHaveBeenCalled();
    });
});

Conclusion

By structuring your mocks in a modular way, you gain control over your testing environment, ensuring that your tests are both reliable and maintainable. This approach is not only scalable but also simplifies the process of managing complex dependencies, making your development process smoother and more efficient.

Call to Action

If you’re developing Obsidian plugins and haven’t tried modular mocks yet, give this setup a try! It will help keep your tests clean, organized, and easy to manage as your project grows. Happy coding!

Sources

Separation of Concerns

Scalability

Consistency

General Resources

  • Bass, L., Clements, P., & Kazman, R. (2012). Software Architecture in Practice (3rd ed.). Addison-Wesley.
  • Kruchten, P. (2004). The Rational Unified Process: An Introduction (3rd ed.). Addison-Wesley.

Post Disclaimer

The information contained on this post is my opinion, and mine alone (with the occasional voice of friend). It does not represent the opinions of any clients or employers.


Posted

in

by