Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Early ai generated unit tests #338

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,6 @@ output-folders-for-testing

# temp files
tmp/

#EarlyAI
.early.coverage
55 changes: 55 additions & 0 deletions README_EARLY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# EarlyAI unit test summary

## Configuration
The repository was already configured to work with JEST.
The tests were created with JEST testing framework.
No special configuration is needed to setup the project except updating jest.config.js collectCoverageFrom:

Original configuration:
collectCoverageFrom: [
"**/code-generator/**/*.{ts,tsx}",
"!**/test/**"
],

Updated to this one to collect coverage correctly where applicable.
collectCoverageFrom: [
"**/code-generator/**/*.{ts,tsx}",
"!**/test/**",
"!**/code-templates/**",
"!**/index.ts",
"!**/entry-points/cli-entry-point.ts",
"!**/entry-points/interactive-cli.tsx",
],


## Target code
src/code-generator
src/code-templates was excluded from testing by the author so it was excluded from this tests run.

## cleanup
For the purposed of this exercise we .skip all existing tests on files so we can see the impact of the newly generated tests by EarlyAI:

practica/src/code-generator/generation-logic/generate-service.test.ts
practica/src/code-generator/test/generator.non-interactive-cli.test.ts

and all tests on files under:
practica/test/

## Results
### src/code-generator Coverage - 96%
### Passed
Test Suites: 9 (Files or describe blocks)
Tests: 48
### Failed
Tests: 0
We did not generate any red tests because this is more of a how-to or example repo rather than a real functional code.



### About Early
Early leverages Generative AI to accelerate development, enhance code quality, and speed up time-to-market. Our AI-driven product generates automated, comprehensive, cost-effective working unit tests, and help catch bugs early, expanding code coverage, and improving overall quality


Learn more at www.startearly.ai or search for EarlyAI on VSCode marketplace, install and user EarlyAI extension to generate unit tests in a click.

Read more on our [blogs](https://www.startearly.ai/early-blog).
7 changes: 7 additions & 0 deletions early.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"generatedTestStructure": "categories",
"testStructureVariant": "siblingFolder",
"showAllTreeSrcFiles": true,
"testFramework": "jest",
"testSuffix": "test"
}
11 changes: 10 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ module.exports = {
collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["**/code-generator/*.{ts,tsx}", "!**/test/**"],
collectCoverageFrom: [
"**/code-generator/**/*.{ts,tsx}",
"!**/test/**",
"!**/code-templates/**",
"!**/index.ts",
"!**/entry-points/cli-entry-point.ts",
"!**/entry-points/interactive-cli.tsx",
],



// The directory where Jest should output its coverage files
coverageDirectory: "test-reports/coverage",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Unit tests for: handleNonInteractiveCommand

import { AppError } from "../../error-handling";
import { factorDefaultOptions } from "../../generation-logic/generation-options";
import { generateApp } from "../../generation-logic/generate-service";
import { nonInteractiveCliTexts, spinner } from "../ui-elements";
import { handleNonInteractiveCommand } from "../non-interactive-cli";

jest.mock("../../generation-logic/generation-options");
jest.mock("../../generation-logic/generate-service");
jest.mock("../ui-elements");

// @ts-ignore
jest.spyOn(process, 'exit').mockImplementation(() => { });

describe("handleNonInteractiveCommand() handleNonInteractiveCommand method", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("Happy Path", () => {
it("should start the spinner, generate the app, and succeed", async () => {
// Arrange
const options = {
installDependencies: true,
overrideIfExists: false,
orm: "typeorm",
webFramework: "express",
targetDirectory: "/my/app",
appName: "myApp",
};
(factorDefaultOptions as jest.Mock).mockReturnValue(options);
(generateApp as jest.Mock).mockResolvedValue(undefined);

// Act
await handleNonInteractiveCommand(options);

// Assert
expect(spinner.start).toHaveBeenCalledWith(
nonInteractiveCliTexts.onStart
);
expect(generateApp).toHaveBeenCalledWith(options);
expect(spinner.succeed).toHaveBeenCalledWith(
nonInteractiveCliTexts.onSucceed
);
});
});

describe("Edge Cases", () => {
it("should handle missing targetDirectory by using process.cwd()", async () => {
// Arrange
const options = {
installDependencies: true,
overrideIfExists: false,
orm: "typeorm",
webFramework: "express",
appName: "myApp",
};
const cwd = process.cwd();
(factorDefaultOptions as jest.Mock).mockReturnValue(options);
(generateApp as jest.Mock).mockResolvedValue(undefined);

// Act
await handleNonInteractiveCommand(options);

// Assert
expect(factorDefaultOptions).toHaveBeenCalledWith(
expect.objectContaining({
targetDirectory: cwd,
})
);
});

it("should handle errors thrown by generateApp", async () => {
// Arrange
const options = {
installDependencies: true,
overrideIfExists: false,
orm: "typeorm",
webFramework: "express",
targetDirectory: "/my/app",
appName: "myApp",
};
const error = new AppError("generation-failed", "Generation failed");
(factorDefaultOptions as jest.Mock).mockReturnValue(options);
(generateApp as jest.Mock).mockRejectedValue(error);

// Act
await handleNonInteractiveCommand(options);

// Assert
expect(spinner.fail).toHaveBeenCalledWith("Generation failed");
expect(process.exit).toHaveBeenCalledWith(1);
});

it("should handle generic errors gracefully", async () => {
// Arrange
const options = {
installDependencies: true,
overrideIfExists: false,
orm: "typeorm",
webFramework: "express",
targetDirectory: "/my/app",
appName: "myApp",
};
const error = new Error("Some unexpected error");
(factorDefaultOptions as jest.Mock).mockReturnValue(options);
(generateApp as jest.Mock).mockRejectedValue(error);

// Act
await handleNonInteractiveCommand(options);

// Assert
expect(spinner.fail).toHaveBeenCalledWith("Some unexpected error");
expect(process.exit).toHaveBeenCalledWith(1);
});

it("should use default error message if error has no message", async () => {
// Arrange
const options = {
installDependencies: true,
overrideIfExists: false,
orm: "typeorm",
webFramework: "express",
targetDirectory: "/my/app",
appName: "myApp",
};
const error = new Error();
(factorDefaultOptions as jest.Mock).mockReturnValue(options);
(generateApp as jest.Mock).mockRejectedValue(error);

// Act
await handleNonInteractiveCommand(options);

// Assert
expect(spinner.fail).toHaveBeenCalledWith(
nonInteractiveCliTexts.onError.default
);
expect(process.exit).toHaveBeenCalledWith(1);
});
});
});

// End of unit tests for: handleNonInteractiveCommand
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Unit tests for: chooseORM

import * as fsExtra from "fs-extra";
import path from "path";

import { generationOptions } from "../../generation-options";
import {
getMicroservicePath,
replacePhraseInFile,
} from "../../string-manipulation-helpers";
import { chooseORM } from "../choose-orm";

jest.mock('fs-extra');

jest.mock("../../string-manipulation-helpers", () => {
const actual = jest.requireActual("../../string-manipulation-helpers"); // This fetches the actual implementations

return {
...actual, // Uses the actual implementations
getMicroservicePath: jest.fn(),
replacePhraseInFile: jest.fn(),
};
});

describe("chooseORM() chooseORM method", () => {
const generatedAppRoot = "mock/generated/app/root";
const mockMicroservicePath = "mock/microservice/path";

beforeEach(() => {
jest.clearAllMocks();
});

describe("Happy Path", () => {
it("should adjust the code to Prisma ORM", async () => {
(getMicroservicePath as jest.Mock).mockReturnValue(mockMicroservicePath);

const options: generationOptions = {
appName: "testApp",
ORM: "prisma",
webFramework: "express",
DBType: "postgres",
mainMicroserviceName: "mainService",
emitBestPracticesHints: true,
targetDirectory: "target/dir",
installDependencies: true,
overrideIfExists: false,
};
(replacePhraseInFile as jest.Mock).mockResolvedValue(undefined);

await chooseORM(generatedAppRoot, options);

expect(getMicroservicePath).toHaveBeenCalledWith(generatedAppRoot);
expect(fsExtra.rm).toHaveBeenCalledWith(
path.join(mockMicroservicePath, "data-access"),
{ recursive: true }
);
expect(fsExtra.rename).toHaveBeenCalledWith(
path.join(mockMicroservicePath, "data-access-prisma"),
path.join(mockMicroservicePath, "data-access")
);
expect(replacePhraseInFile).toHaveBeenCalledTimes(4);
});

it("should adjust the code to Sequelize ORM", async () => {
const options: generationOptions = {
appName: "testApp",
ORM: "sequelize",
webFramework: "express",
DBType: "postgres",
mainMicroserviceName: "mainService",
emitBestPracticesHints: true,
targetDirectory: "target/dir",
installDependencies: true,
overrideIfExists: false,
};
(replacePhraseInFile as jest.Mock).mockResolvedValue(undefined);

await chooseORM(generatedAppRoot, options);

expect(getMicroservicePath).toHaveBeenCalledWith(generatedAppRoot);
expect(fsExtra.rm).toHaveBeenCalledWith(
path.join(mockMicroservicePath, "data-access-prisma"),
{ recursive: true }
);
expect(replacePhraseInFile).toHaveBeenCalledTimes(3); // Expecting 3 calls for Sequelize adjustments
});
});

describe("Edge Cases", () => {
it("should handle an unsupported ORM gracefully", async () => {
const options: generationOptions = {
appName: "testApp",
ORM: "unsupported" as any, // Simulating an unsupported ORM
webFramework: "express",
DBType: "postgres",
mainMicroserviceName: "mainService",
emitBestPracticesHints: true,
targetDirectory: "target/dir",
installDependencies: true,
overrideIfExists: false,
};

await expect(chooseORM(generatedAppRoot, options)).resolves.not.toThrow();
expect(getMicroservicePath).toHaveBeenCalledWith(generatedAppRoot);
expect(fsExtra.rm).not.toHaveBeenCalled(); // No folder should be removed
expect(replacePhraseInFile).not.toHaveBeenCalled(); // No phrases should be replaced
});
});
});

// End of unit tests for: chooseORM
Loading
Loading