Testing and Debugging
Welcome to the guide on testing and debugging your Projen projects! If you're new to Projen or testing in general, don't worry — this page is designed to help you understand and effectively use testing and debugging techniques in your projects.
Testing
In Projen projects, the most common way to run tests is using a testing framework called Jest. Jest is a JavaScript Testing Framework with a focus on simplicity.
When you set up your Projen project, you can configure Jest in your projenrc.js file like this:
const project = new NodeProject({
// ...
jestOptions: {
jestConfig: {
testMatch: ["**/*.test.ts"],
},
},
});
However, most of the time, the default Jest configuration works fine.
jest or any other testing framework can be used to test projen projects. The following sections will describe some of the common testing patterns in jest. Feel free to contribute best practices and tips for other testing frameworks.
Snapshot testing
The most straightforward method for testing projen projects is to use jest's snapshot testing feature. This feature allows you to take a snapshot of the project's generated files and compare them to the expected output.
Here's a basic example:
import { Testing } from "projen/lib/testing";
import { MyProject } from "../src/my-project";
test("snapshot", () => {
const project = new MyProject();
const snapshot = Testing.synth(project);
expect(snapshot).toMatchSnapshot();
});
The above test will generate a snapshot of the entire project and compare it to the expected output. If the snapshot does not match, the test will fail.
You'll quickly find that these types of snapshot tests are unwieldy - it captures the entire project and it's hard to pinpoint exactly what changed. However, you can adjust the test to look at the contents of a single file that was generated:
import { Testing } from "projen/lib/testing";
import { MyProject } from "../src/my-project";
test("snapshot", () => {
const project = new MyProject();
const snapshot = Testing.synth(project);
expect(snapshot["README.md"]).toMatchSnapshot();
});
This test will only compare the contents of the README.md
file to the expected
output. In other words, your snapshot file will only contain the contents of
the README.md
file and not the entire project tree.
Generally, if you just want to make sure that the contents of a single file are consistent, favor snapshot testing over comparing the contents directly.
Testing file creation
Testing.synth()
returns a map of all the files that were generated by the
project. You can use this map to identify whether specific files were created.
import { Testing } from "projen/lib/testing";
import { MyProject } from "../src/my-project";
test("snapshot", () => {
const project = new MyProject();
const snapshot = Testing.synth(project);
expect(snapshot[".github/workflows/release.yml"]).toBeDefined();
});
Note that this type of testing only identifies whether the file exists. It does not do any content comparison. You likely will want to use this type of testing in conjunction with snapshot testing.
Debugging
Unit testing is great for testing individual components of your project, but sometimes it's easier to actually build the project locally and debug it.
Debugging with yarn
If you're using yarn, you can use the yarn link
command to build your project
locally and link it to your project. For example, if you have a projen project
in ~/my-project
and you want to debug it in ~/my-app
, you can run the
following commands:
# In ~/my-project
yarn link
# In ~/my-app
yarn link my-project
yarn projen new my-project --from my-project
Assuming my-project
is the name of your project in ~/my-project/projenrc.js
.
For more information, see the yarn documentation.
Debugging with npm
If you prefer NPM, the process is similar:
# In ~/my-project
npm link
# In ~/my-app
npm link my-project
npx projen new my-project --from my-project
Debugging with pnpm
For PNPM users, the steps are also quite straightforward:
# In ~/my-project
pnpm link
# In ~/my-app
pnpm link my-project
npx projen new my-project --from my-project
Common Issues and Solutions
Non-Deterministic Snapshot Tests
Snapshot tests should be predictable, but sometimes they fail randomly. This can happen if your project attempts to modify a file that's already in your project tree.
For example, if your project is trying
to create a README.md
file, but the project already has a README.md
file,
you either run into a race condition or one file will overwrite the other
consistently. This is especially common when extending existing projen
projects.
Resolving this issue can be challenging. If you are extending an existing
projen project, you can try removing your README.md
file code and see if it still
exists in the project tree with a snapshot test. If it does, then something else
in your project code is already creating the file, and you'll need to identify
what that is and either remove it or override it.
Example solution
In the example below, we're extending the NodeProject
class, which already
creates a README.md
file. In fact, all classes that extend GitHubProject
will create a README.md
file by default. This behavior is controlled by the
readme
option in the GitHubProjectOptions
interface.
We can remove the README.md
file from the base project, which should allow
the one we've hypothetically added in CustomNodeProject
to be created.
const project = new CustomNodeProject({
// ...
readme: false, // This will disable the readme file in the underlying NodeProject
});
project.synth();
Alternately, since we know that the README.md
file is already being created,
we can remove the code that creates it in CustomNodeProject
and just use the
one from the base class.
For classes that do not have a readme
option, you can use the tryRemoveFile
option to remove the file from the project tree.
const project = new CustomNodeProject({
// ...
});
project.tryRemoveFile("README.md");
Note, however, that this will probably also remove the README.md
file created
from CustomNodeProject
, so in this particular case, it's probably better to
just disable the README.md
file in the base class or use it instead of a
custom implementation.