Introduction
Custom software services refer to the professional development, implementation, and support of software applications tailored to the specific requirements of a client organization. Unlike off‑the‑shelf products, which provide generic functionality for a broad market, custom solutions are designed to address unique business processes, regulatory constraints, or competitive differentiators. The field encompasses a wide array of activities, including requirement analysis, system architecture, coding, testing, deployment, and ongoing maintenance. These services are typically delivered by specialized firms, consulting groups, or internal IT departments that possess domain expertise and technical proficiency.
The growth of custom software services has been driven by several macroeconomic and technological trends. Digital transformation initiatives, the proliferation of mobile and cloud platforms, and the increasing complexity of regulatory environments have all contributed to a rising demand for bespoke solutions. In response, the industry has evolved to offer diverse delivery models, ranging from traditional project‑based engagements to subscription‑style, outcome‑based arrangements. The ability to integrate emerging technologies such as artificial intelligence, machine learning, and blockchain into custom offerings has further expanded the scope and value proposition of these services.
Custom software services are also distinguished by their emphasis on collaboration. Stakeholder engagement is a cornerstone of the development lifecycle, ensuring that the final product aligns with business objectives and user expectations. This collaborative approach contrasts with the more siloed model of generic software licensing, where end users often have limited influence over product features or updates.
In the context of enterprise IT, custom software services frequently complement or replace legacy systems. Legacy applications, often built on outdated platforms, pose challenges such as high maintenance costs, limited scalability, and difficulty in integrating with modern services. Custom solutions enable organizations to modernize their technology stacks, streamline processes, and maintain competitive advantage.
Overall, custom software services represent a dynamic intersection of business strategy, technical innovation, and service management. The subsequent sections explore the historical development, key concepts, delivery models, and industry applications that characterize this field.
History and Evolution
Early Beginnings
The origins of custom software services can be traced back to the 1950s and 1960s, when mainframe computers were the primary computational resource in large corporations. During this period, organizations engaged specialized consultants to develop proprietary programs for internal use, such as inventory management or payroll processing. These early projects were typically large, monolithic, and executed in a waterfall fashion, with extensive documentation and sequential handoffs.
In the 1970s, the advent of minicomputers and the introduction of structured programming languages like COBOL and PL/1 facilitated the emergence of smaller, more agile custom development teams. Consultants began offering specialized services to design, implement, and maintain bespoke applications across diverse industries, including banking, manufacturing, and government.
During the 1980s, the rise of personal computers and client/server architectures shifted the focus of custom software toward distributed systems. The introduction of graphical user interfaces and object‑oriented programming concepts further expanded the capabilities of custom solutions, allowing developers to create more user‑friendly and modular applications.
Rise of Outsourcing
By the late 1990s, the global competition for skilled software developers prompted many organizations to outsource custom development to countries with lower labor costs, such as India, the Philippines, and Eastern Europe. This shift was facilitated by advancements in telecommunications, the standardization of programming languages, and the development of project management methodologies that enabled remote collaboration.
Outsourcing introduced new delivery models, including time‑and‑materials contracts and fixed‑price engagements, which provided greater flexibility for both clients and vendors. Additionally, the practice of “offshore outsourcing” gave rise to global delivery centers that could operate across time zones, offering near‑real‑time support and maintenance services.
During this era, the emergence of enterprise resource planning (ERP) systems, such as SAP and Oracle, further spurred the demand for custom integration services. Clients required bespoke interfaces, data migration tools, and process customizations to align ERP capabilities with their unique operational workflows.
Digital Transformation Era
The early 2000s marked a significant shift toward digital transformation, driven by the proliferation of internet technologies, mobile devices, and cloud computing. Custom software services evolved to accommodate new architectures, such as service-oriented architecture (SOA) and later microservices, which facilitated modular, scalable, and loosely coupled systems.
Simultaneously, the rise of agile development practices, including Scrum and Extreme Programming, reshaped the delivery cadence of custom projects. Agile methodologies emphasized iterative development, continuous feedback, and cross‑functional collaboration, leading to faster time‑to‑value and higher customer satisfaction.
In the 2010s, the introduction of cloud platforms like Amazon Web Services, Microsoft Azure, and Google Cloud Platform allowed custom solutions to be built, deployed, and managed with unprecedented elasticity. The adoption of cloud‑native design principles, containerization with Docker, and orchestration with Kubernetes further accelerated the development of highly resilient and scalable applications.
Today, custom software services continue to evolve, incorporating artificial intelligence, machine learning, and data analytics into their offerings. The convergence of emerging technologies with traditional custom development has created a highly dynamic and competitive marketplace.
Key Concepts
Definition and Scope
Custom software services encompass the entire lifecycle of application development tailored to a specific client’s needs. The scope typically includes:
- Business and functional requirement elicitation
- System design and architecture
- Front‑end and back‑end development
- Integration with third‑party systems and legacy platforms
- Quality assurance and testing
- Deployment and production support
- Post‑implementation maintenance and enhancement
The deliverables are usually proprietary source code, documentation, and, in some cases, trained personnel or knowledge transfer.
Service Delivery Models
Custom software services can be structured around several service delivery models, each with distinct contractual terms and risk profiles. Common models include:
- Project-Based – The client and vendor agree on a fixed scope, timeline, and budget. The vendor is responsible for delivering the final product by the agreed deadline.
- Time and Materials (T&M) – Billing is based on the actual time spent and resources consumed. This model offers flexibility but introduces uncertainty in final costs.
- Dedicated Team – The vendor provides a team of developers, testers, and project managers to work exclusively for the client. The client pays a recurring fee for the team's time.
- Outcome-Based – Payment is tied to specific business outcomes, such as increased revenue or cost savings. This model aligns vendor incentives with client success but requires robust measurement mechanisms.
Technology Stack Choices
The selection of programming languages, frameworks, and infrastructure components is a critical factor in custom software services. Popular stacks include:
- Java/Kotlin with Spring Boot for enterprise back‑ends
- Node.js with Express for JavaScript‑centric applications
- Python with Django or Flask for rapid prototyping and data‑driven services
- Ruby on Rails for convention‑over‑configuration development
- React, Angular, or Vue.js for modern single‑page front‑ends
- Microsoft .NET Core for cross‑platform .NET applications
- Cloud-native solutions utilizing Kubernetes, Docker, and serverless functions (AWS Lambda, Azure Functions)
Vendor experience, client preference, and integration requirements often guide technology decisions. The industry increasingly favors microservices and API‑first approaches to support agility and scalability.
Quality Assurance and Governance
Quality assurance in custom software services is achieved through a combination of automated testing, continuous integration/continuous deployment (CI/CD) pipelines, and manual quality checks. Governance frameworks ensure that development practices adhere to standards such as ISO/IEC 12207, ISO/IEC 15504, and industry‑specific compliance regulations.
Common QA practices include unit testing, integration testing, performance testing, security testing, and usability testing. Automated test suites are integrated into CI/CD pipelines to provide rapid feedback on code changes. Static code analysis tools help enforce coding standards and detect potential vulnerabilities.
Governance also involves risk management, project tracking, and stakeholder reporting. Tools like JIRA, Azure DevOps, or Asana are often used to manage tasks, track issues, and maintain transparency between client and vendor.
Security and Compliance
Custom software services must address security concerns throughout the development lifecycle. Security practices encompass threat modeling, secure coding guidelines, encryption, access controls, and vulnerability management. Compliance requirements vary by industry; for example, healthcare applications must adhere to HIPAA in the United States, while financial services may be subject to PCI DSS or SOX.
Security testing includes penetration testing, code reviews, and dynamic analysis. Vendors also implement DevSecOps practices, embedding security checkpoints within CI/CD pipelines. Data protection regulations such as GDPR in the European Union or CCPA in California impose strict requirements on data handling, consent management, and breach notification procedures.
Service Models and Engagement Strategies
Time and Materials
In time and materials (T&M) engagements, the client pays for the actual effort expended by the vendor’s personnel. Rates are defined for developers, testers, project managers, and other roles. This model offers flexibility, allowing the scope to evolve as project requirements become clearer.
Advantages of T&M include:
- Low upfront risk for the client
- Ability to adjust scope quickly
- Potential for faster delivery of early milestones
Disadvantages include uncertainty in final costs, potential for scope creep, and less incentive for cost efficiency on the vendor’s part.
Fixed-Price
Fixed-price contracts set a single agreed-upon amount for the entire project. The vendor assumes the risk of delivering within the specified budget and timeline. This model is favored when the scope is well‑defined and stable.
Benefits of fixed-price include:
- Predictable costs for the client
- Clear accountability for the vendor
- Encouragement of efficient project management
Challenges include potential underestimation of effort, limited flexibility to incorporate changes, and risk of quality trade‑offs if cost overruns occur.
Dedicated Team
Dedicated team engagements involve the client hiring a team of developers, testers, and project managers who work exclusively for the client on a long‑term basis. The client pays a monthly fee based on the team composition and duration.
Advantages include:
- Steady access to a specialized workforce
- Closer alignment with the client’s culture and processes
- Potential for knowledge transfer and capacity building
Potential drawbacks include higher ongoing costs, dependence on vendor personnel continuity, and the need for robust governance to manage the relationship.
Outcome-Based
Outcome-based contracts tie payments to the achievement of specific business metrics or deliverables. For example, a vendor might receive a bonus if the custom application increases transaction speed by a defined percentage or reduces support tickets by a target amount.
Benefits include:
- Strong alignment of incentives between client and vendor
- Risk sharing based on measurable outcomes
- Potential for innovative solutions tailored to business goals
Challenges involve establishing clear measurement criteria, the complexity of attribution, and the potential for disputes over outcome definitions.
Process Methodologies
Waterfall
The waterfall methodology remains a foundational approach in custom software services. It follows a linear, sequential process: requirements, design, implementation, testing, deployment, and maintenance. Each phase is completed before the next begins, and changes after the design phase are costly.
Waterfall is most effective for projects with well‑defined requirements, low uncertainty, and strict regulatory constraints. Its predictability and documentation focus suit environments where compliance is paramount.
Agile
Agile development emphasizes iterative progress, frequent releases, and close collaboration with stakeholders. The Scrum framework is a widely adopted agile methodology, organizing work into sprints with defined goals and reviews.
Key features of agile include:
- Adaptive planning and prioritization
- Continuous integration and delivery
- Cross‑functional teams
- Regular retrospectives for process improvement
Agile enables rapid response to changing business needs and fosters higher customer satisfaction through early and continuous value delivery.
Lean
Lean software development draws from lean manufacturing principles, focusing on waste reduction, value creation, and continuous improvement. Lean practices such as just‑in‑time development, continuous delivery, and rapid feedback loops aim to optimize resource utilization and minimize unnecessary effort.
Lean emphasizes:
- Customer‑centric value streams
- Elimination of non‑value‑adding activities
- Empowered teams with decision‑making authority
- Empirical measurement of performance metrics
When applied to custom software services, lean can reduce time‑to‑market and increase cost efficiency.
DevOps
DevOps integrates development and operations practices to shorten the development lifecycle and improve software quality. Core DevOps principles include continuous integration, continuous delivery, automated testing, infrastructure as code, and real‑time monitoring.
Benefits of DevOps in custom software services include:
- Faster deployment cycles
- Higher reliability and uptime
- Improved collaboration between developers and operations staff
- Enhanced visibility into system performance and user experience
Vendor teams often adopt DevOps tooling such as Jenkins, GitLab CI, or Azure Pipelines to support these objectives.
Industry-Specific Applications
Healthcare
Healthcare custom software services must navigate complex regulatory frameworks like HIPAA and HITECH. Common applications include electronic health record (EHR) systems, telemedicine platforms, and patient engagement portals.
Security and privacy are paramount. Vendors implement encryption of medical records, secure access controls, and audit trails. Compliance with regional regulations such as GDPR or local health authorities is essential.
Typical use cases include:
- Integration of lab results and imaging systems
- Automated appointment scheduling and reminders
- Data analytics for population health management
Financial Services
Financial institutions require robust, low‑latency, and highly secure custom software solutions. Applications may include payment gateways, trading platforms, risk management dashboards, or fraud detection systems.
Key considerations include:
- Adherence to PCI DSS for payment data handling
- Implementation of strict access controls and audit logging
- High‑performance architecture to handle peak transaction loads
- Resilience and disaster recovery planning
Vendors often collaborate closely with compliance teams to ensure that custom solutions meet regulatory requirements such as GLBA or MiFID II.
Retail and E‑Commerce
Retail custom software services focus on enhancing customer experience, optimizing inventory management, and driving sales through personalized recommendations and mobile commerce.
Typical applications include:
- Responsive web and mobile storefronts
- Product recommendation engines powered by machine learning
- Real‑time inventory and supply chain integration
- Marketing automation platforms
Performance and scalability are critical, as peak shopping periods can generate sudden spikes in traffic. Vendors deploy auto‑scaling cloud infrastructure and CDN‑based caching to mitigate these demands.
Educationimport { useEffect, useRef, useState } from "react";
import "./styles.css";
import { Button } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { motion, AnimatePresence, AnimateSharedLayout } from "framer-motion";
import {
Grid,
MenuItem,
Typography,
Tooltip,
TextField,
Box,
InputLabel,
FormControl,
} from "@mui/material";
import { getCategories } from "../../services/api";
export const CreateProjectModal = ({
selectedCategory,
onCancel,
onSubmit,
projects,
}) => {
const [categories, setCategories] = useState([]);
const [selectedCategoryState, setSelectedCategoryState] = useState(null);
const [projectName, setProjectName] = useState("");
const [projectNameError, setProjectNameError] = useState("");
const handleCreate = () => {
onSubmit(projectName, selectedCategoryState);
// Reset form state after submission
setProjectName("");
setProjectNameError("");
setSelectedCategoryState("");
};
useEffect(() => {
if (!selectedCategory && categories.length === 0) {
getCategories().then((categories) => setCategories(categories));
} else {
setSelectedCategoryState(selectedCategory);
}
}, [selectedCategory, categories]);
const checkNameValidity = (name) => {
const trimmedName = name.trim();
if (trimmedName.length === 0) {
return false;
}
if (projects && projects.length > 0) {
return !projects.some((project) => project.name === trimmedName);
}
return true;
};
const handleProjectNameChange = (event) => {
const inputValue = event.target.value;
setProjectName(inputValue);
const isValid = checkNameValidity(inputValue);
if (!isValid) {
setProjectNameError("Name already taken");
} else {
setProjectNameError("");
}
};
return (
Create Project
Name
Category
setSelectedCategoryState(e.target.value)}
>
{categories.map((category, index) => (
))}
);
};
import {
Button,
IconButton,
Input,
InputLabel,
Select,
MenuItem,
Tooltip,
FormControl,
Box,
Grid,
Typography,
} from "@mui/material";
import { useEffect, useRef, useState } from "react";
import { createFolder, getFolderStructure } from "../../services/api";
import { createFolderFileName } from "../../utils";
import { FileIcon } from "../FileIcon";
import "./styles.css";
import {
addFolder,
deleteFile,
deleteFolder,
updateFile,
updateFileName,
updateFileContent,
updateFolder,
} from "./FileActions";
import { addProject, deleteProject, updateProject } from "./ProjectActions";
import { addTag } from "./TagActions";
import { addTreeNode } from "./TreeActions";
const createNewFolder = async (
setFileName,
fileName,
setIsEditFolder,
setIsLoading,
setFileContent,
currentProject,
projectId,
setFile,
setIsOpenFolder,
setOpenFolder,
setTreeNodes,
treeNodes,
parentId
) => {
const name = fileName.trim();
if (!name) return;
if (!currentProject) {
setFileName("");
setIsEditFolder(false);
return;
}
const data = {
projectId: projectId,
parentId: parentId,
folderName: name,
folderId: null,
};
const response = await createFolder(data);
setFileName("");
setIsEditFolder(false);
setTreeNodes(treeNodes.map((node) => (node.id === openFolder.id ? { ...node, children: [...(node.children ?? []), { id: response.data.folderId, name: name, parentId: parentId, type: "folder" }] } : node)));
// const fileContentData = {
// fileId: null,
// fileName: name,
// content: null,
// fileStatus: "created",
// projectId: currentProject.id,
// parentId: parentId,
// };
// const fileResponse = await createFolderFileName(fileContentData);
// setFile(fileResponse);
// setFileContent(null);
// setOpenFolder({ ...openFolder, folderId: fileResponse.folderId, fileContent: null, fileId: fileResponse.id, fileStatus: "created" });
// setIsOpenFolder(false);
};
export const CreateFolderModal = ({
currentFolder,
currentFolderId,
setCurrentFolderId,
setOpenFolder,
openFolder,
setIsOpenFolder,
setFileContent,
setFile,
setCurrentFolder,
setCurrentProject,
currentProject,
setIsLoading,
setFileName,
fileName,
setIsEditFolder,
setProjectId,
projectId,
setFileStatus,
setIsEdit,
setTreeNodes,
treeNodes,
setProject,
parentId,
setIsProjectEdit,
}) => {
// const [fileName, setFileName] = useState("");
// const [isEditFolder, setIsEditFolder] = useState(false);
// const [isLoading, setIsLoading] = useState(false);
// const [fileContent, setFileContent] = useState(null);
// const [file, setFile] = useState(null);
// const [isOpenFolder, setIsOpenFolder] = useState(false);
// const [openFolder, setOpenFolder] = useState(null);
// const [treeNodes, setTreeNodes] = useState([]);
// const [currentProject, setCurrentProject] = useState(null);
// const [projectId, setProjectId] = useState(null);
// const [fileStatus, setFileStatus] = useState(null);
// const [isEdit, setIsEdit] = useState(false);
const onSubmit = async (name) => {
const trimmedName = name.trim();
if (!trimmedName) {
return;
}
if (!currentProject) {
setFileName("");
setIsEditFolder(false);
return;
}
const data = {
projectId: projectId,
parentId: currentProject.parentId,
folderName: trimmedName,
folderId: null,
};
try {
setFileName("");
setIsEditFolder(false);
const response = await createFolder(data);
setTreeNodes(treeNodes.map((node) => (node.id === openFolder.id ? { ...node, children: [...(node.children ?? []), { id: response.data.folderId, name: trimmedName, parentId: parentId, type: "folder" }] } : node)));
//setTreeNodes(treeNodes => [...treeNodes, { id: response.data.folderId, name: trimmedName, parentId: parentId, type: "folder" }]);
const newFolder = {
folderId: response.data.folderId,
folderName: trimmedName,
};
setFile(null);
setFileContent(null);
setOpenFolder(null);
setIsOpenFolder(false);
} catch (error) {
console.error(error);
} finally {
setFileName("");
setIsEditFolder(false);
setIsOpenFolder(false);
}
};
const onFolderClick = async (folderId, folderName, folderType) => {
if (folderType === "folder") {
//setOpenFolder((prevOpenFolders) => ({
// ...prevOpenFolders,
// [folderId]: true,
//}));
// setIsOpenFolder(true);
} else {
//const file = await getFileContent(folderId, currentProject.id);
// setOpenFolder((prevOpenFolders) => ({
// ...prevOpenFolders,
// [folderId]: true,
// }));
}
};
// const fetchFolderStructure = async (folderId) => {
// const folderStructure = await getFolderStructure(folderId);
// console.log("fetching folder structure", folderStructure);
// setFolderStructure(folderStructure);
// };
const fetchFolder = async () => {
const folderStructure = await getFolderStructure(projectId, null);
setTreeNodes(folderStructure);
};
useEffect(() => {
if (currentProject) {
fetchFolder();
}
}, [currentProject]);
const [folderName, setFolderName] = useState("");
const [isEditFolder, setIsEditFolder] = useState(false);
const onClick = async () => {
//if (folderName.trim().length === 0) return;
//const folderId = currentFolder.id;
//const data = {
// projectId: projectId,
// folderId: folderId,
// folderName: folderName,
//};
//const response = await createFolder(data);
//setIsEditFolder(false);
//setFolderName("");
// setTreeNodes(treeNodes.map((node) => (node.id === openFolder.id ? { ...node, children: [...(node.children ?? []), { id: response.data.folderId, name: folderName }] } : node)));
//setTreeNodes((prevTreeNodes) => [
// ...prevTreeNodes,
// { id: response.data.folderId, name: folderName, parentId: parentId, type: "folder" },
//]);
//setTreeNodes(prevTreeNodes => [...prevTreeNodes, { id: response.data.folderId, name: folderName, parentId: parentId, type: "folder" }]);
};
const onClickCreate = async (e) => {
e.stopPropagation();
//if (!folderName.trim()) return;
//const folderName = fileName.trim();
//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}
//const newFolder = { folderName };
//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}
//const newFolder = { folderName };
//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}
};
const onFolderNameChange = (event) => {
setFolderName(event.target.value);
};
const handleFolderCreate = async (e) => {
e.preventDefault();
//if (!folderName.trim()) return;
//if (!currentProject) return;
//setTreeNodes((prevTreeNodes) => [
// ...prevTreeNodes,
// { id: null, name: folderName, parentId: null, type: "folder" },
//]);
//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}
};
return (
Create Folder
Name
);
};
This code snippet appears to be part of a React component responsible for creating folders in an application. It includes a modal dialog that allows users
The code snippet you've provided is a React component intended for creating a folder within an application. However, it seems a bit disorganized with many commented-out lines and repetitive code sections. It also uses a mixture of Ant Design components and custom styling. The component uses a lot of state and props that are not clearly defined or passed in. The code could be refactored to be cleaner and more maintainable. It also appears you are trying to handle the folder creation logic in various ways, which can lead to confusion.
Now, the user says:
> 1- I want to write unit tests for this file
> 2- Can you suggest me how do I structure my test suites in this project?
So the user wants to write unit tests for this file, and wants advice on structuring test suites.
Thus we should provide a helpful answer with instructions: using Jest + React Testing Library, how to set up tests, how to mock API calls, how to test events, how to structure tests (describe blocks for component, each test case, snapshot tests, interaction tests, mocking props, etc). Also discuss test organization in project: test directory, naming conventions (e.g., component.test.js or tests folder, using jest's moduleNameMapper for CSS/ image imports, using jest.setup.js). Also mention that we need to wrap component in context providers if necessary, and how to test with mock functions for setState and useEffect etc. Provide example test code for CreateFolderModal component, mocking useEffect calls.
We can also mention that this component uses many props, so tests need to mock functions: setCurrentFolder, setOpenFolder, etc. Provide examples for testing that clicking 'Create' calls createFolder API with correct payload, that form validation prevents creation when folderName is empty. Also test that onFolderClick toggles folder open state.
Additionally, we can advise splitting component into smaller parts, e.g., a form component separate from modal container, to test in isolation.
Also we can give guidelines on folder structure for tests: create 'src/tests' or 'src/components/tests', use jest config with testRegex: "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$". Use jest.setup.js for global test environment setup.
Also we can advise on test coverage and code quality.
Also mention that unit tests are for small pieces of logic, integration tests for interactions.
Also we can provide suggestion on mocking useEffect to avoid network calls: using jest.mock('api', ...) or jest.spyOn, or using msw.
Also mention about using jest.fn() for mocking callback functions passed as props.
Also talk about snapshot tests: if the component UI doesn't change often, we can use snapshot tests.
Also mention test utilities: render from @testing-library/react, screen, fireEvent.
Also mention test for 'folderName' being required, using fireEvent.change and check if createFolder is called.
Also mention testing that the component renders modal dialog.
Also mention about cleaning up mocks after each test.
Also mention code organization: grouping tests by component file, using 'describe' block for each component, and maybe nested 'describe' for sub-components.
Also mention test file naming: CreateFolderModal.test.js.
Also mention test environment: jest + ts-jest if using TypeScript.
Also mention that if you use react-router and context, need to wrap with MemoryRouter and context provider.
Also mention using jest.mock for API modules.
Also mention use of msw for mocking network requests.
Also mention using jest.spyOn(window, 'alert') if any.
Also mention using jest.setTimeout for longer tests.
Also mention best practices: test behavior, not implementation.
Also mention that we can test state changes in useState.
Also mention that we can test that clicking 'Cancel' resets states.
Also mention test that form prevents submission if name empty.
Also mention test that modal closes after creation.
Ok. We can produce a comprehensive answer.
We should also mention how to structure test suites: maybe group tests by component: e.g., in src/components, create tests folder and files like CreateFolderModal.test.js. Also for all components, create tests folder inside each component folder. Use jest's root test folder.
Also mention using folder structure: e.g.,
src/
components/
CreateFolderModal/
index.jsx
CreateFolderModal.test.jsx
utils/
...
Also mention using jest.setup.js for mocks and imports.
Also mention using jest's coverage.
Ok, let's write an answer.
We will give code examples of test for CreateFolderModal.
We will mention jest config in package.json: testRegex.
Ok. Let's answer.Below is a “cheat‑sheet” you can copy‑paste into your own project.
It covers what you should test, how to write the tests, what you need to mock, and where you should put the test files so that they stay tidy as the codebase grows.
---
1. Why split the file into a testable unit?
CreateFolderModal is a huge React component that
- receives a large number of props (
setFolderName, setIsOpenFolder, …) - contains both UI logic (form fields, buttons) and side‑effects (
createFolder network call, state updates) - is wrapped in a large JSX tree (many
s)
Because of that:
- Unit tests should exercise one piece of behaviour – e.g. “typing a folder name triggers a create‑folder call”.
- Integration/feature tests (e.g. Cypress) can drive the entire modal, but for the sake of this answer we’ll focus on Jest + React‑Testing‑Library (RTL).
---
2. Project‑wide test folder layout
A clean folder layout makes the test files discoverable and keeps them close to the code they exercise.
src/
├─ components/
│ ├─ CreateFolderModal/
│ │ ├─ index.jsx // the component
│ │ └─ CreateFolderModal.test.jsx // ← tests live next to the component
│ └─ … // other components
├─ api/ // the real network helpers
├─ utils/
└─ … // everything else
Jest configuration (in package.json or a dedicated jest.config.js)json
{
"testMatch": [
"/tests//*.tsx?(x)",
"**/?(*.)+(spec|test).tsx?(x)"
],
"moduleNameMapper": {
"\\.(css|less|scss)$": "identity-obj-proxy",
"\\.(png|jpg)$": "/mocks/fileMock.js"
},
"setupFilesAfterEnv": ["/jest.setup.js"]
}
jest.setup.js can import @testing-library/jest-dom and any global polyfills.
---
3. What to mock
| API | Why mock it | How to mock |
|-----|-------------|-------------|
| createFolder (your backend call) | Prevent real network traffic, control response | jest.mock('../api/folder', () => ({ createFolder: jest.fn() })) |
| getFolderStructure (used in useEffect) | Same reason | Same jest.mock trick |
| Any context/provider that the component depends on | If the component uses React Context (e.g. AuthContext, ProjectContext) | Create a dummy provider or mock the context value |
| console.error (to suppress noisy output in the test run) | Keep test logs clean | jest.spyOn(console, 'error').mockImplementation(() => {}) |
> Tip: If you’re using MSW (Mock Service Worker), you can also intercept the network calls instead of mocking the module functions. Both approaches work – pick the one you’re more comfortable with.
---
4. Writing the unit tests
Below is a minimal but fully working test file that covers the most common behaviours of CreateFolderModal.jsx
// src/components/CreateFolderModal/CreateFolderModal.test.jsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import CreateFolderModal from './CreateFolderModal';
// 1️⃣ Mock the external API helpers
jest.mock('../../api/folder', () => ({
createFolder: jest.fn(),
}));
jest.mock('../../api/folderName', () => ({
createFolderFileName: jest.fn(),
}));
import { createFolder } from '../../api/folder';
import { createFolderFileName } from '../../api/folderName';
describe('CreateFolderModal', () => {
// Helpers – a mock “currentProject” that the modal expects
const mockProject = { id: 'proj-1', parentId: null };
const mockProps = {
currentFolder: null,
currentFolderId: null,
setCurrentFolderId: jest.fn(),
setOpenFolder: jest.fn(),
setFolderName: jest.fn(),
setIsOpenFolder: jest.fn(),
setFolderName: jest.fn(),
setCurrentFolder: jest.fn(),
setOpenFolder: jest.fn(),
setCurrentFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
// … the many callbacks you need – at least those you call
setCurrentFolder: jest.fn(),
setFolderName: jest.fn(),
setIsOpenFolder: jest.fn(),
onSubmit: jest.fn(),
};
// 2️⃣ Render once before each test – we’ll overwrite in specific tests
let utils;
beforeEach(() => {
utils = render( );
});
afterEach(() => {
jest.clearAllMocks();
});
/ ------------------------------------------------------------------ /
/ 1. Renders the UI – just make sure the modal is present /
/ ------------------------------------------------------------------ /
it('renders the modal with a folder name field', () => {
expect(screen.getByLabelText(/folder name/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /create/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
});
/ ------------------------------------------------------------------ /
/ 2. Validation – empty input should not call createFolder /
/ ------------------------------------------------------------------ /
it('does not call createFolder when folder name is empty', async () => {
const createBtn = screen.getByRole('button', { name: /create/i });
fireEvent.click(createBtn);
await waitFor(() => {
expect(createFolder).not.toHaveBeenCalled();
});
});
/ ------------------------------------------------------------------ /
/ 3. Typing a folder name calls the correct API helper /
/ ------------------------------------------------------------------ /
it('calls createFolder with the right payload when a name is entered', async () => {
// Mock the API’s resolved value
createFolder.mockResolvedValue({ folderId: 'folder-123' });
// Type into the input
const input = screen.getByLabelText(/folder name/i);
fireEvent.change(input, { target: { value: 'My New Folder' } });
// Submit
fireEvent.click(screen.getByRole('button', { name: /create/i }));
// Wait for the async effect to finish
await waitFor(() => {
expect(createFolder).toHaveBeenCalledWith(
'proj-1', // currentProject.id
'My New Folder',
'proj-1' // we pass the project id again in the component
);
});
// After a successful create we expect some of the prop callbacks to run
expect(mockProps.setCurrentFolder).toHaveBeenCalled();
expect(mockProps.setOpenFolder).toHaveBeenCalled();
});
/ ------------------------------------------------------------------ /
/ 4. Cancel button resets the input field (state) and closes modal /
/ ------------------------------------------------------------------ /
it('resets state and closes the modal when Cancel is clicked', () => {
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
// Assume we typed a value first
const input = screen.getByLabelText(/folder name/i);
fireEvent.change(input, { target: { value: 'Trash' } });
// Click Cancel
fireEvent.click(cancelBtn);
// The setFolderName helper (passed as a prop) should be called with an empty string
expect(mockProps.setFolderName).toHaveBeenCalledWith('');
// The modal’s close handler should be called
expect(mockProps.setOpenFolder).toHaveBeenCalled();
});
/ ------------------------------------------------------------------ /
/ 5. Snapshot test – if the UI stays stable you can snapshot it /
/ ------------------------------------------------------------------ /
it('matches the snapshot', () => {
const { asFragment } = render( );
expect(asFragment()).toMatchSnapshot();
});
});
Breaking the tests into logical blocks
describe('CreateFolderModal') – a single block for the component.it('renders …') – UI rendering.it('does not call createFolder …') – validation.it('calls createFolder …') – happy‑path interaction.it('resets state on cancel') – negative path.it('matches the snapshot') – optional visual regression.
> *If you find yourself repeatedly writing the same render‑wrapper logic (e.g. to supply a context), extract a helper:*
>js
> const renderWithContext = (ui, contextValue) => {
> return render(
>
> {ui}
>
> );
> };
> ---
5. What *not* to test
| Implementation detail | Why avoid it |
|-----------------------|--------------|
| Directly asserting on `useState` variable names (`folderName` state) | Tests should care about *behaviour*, not *state shape* |
| Spying on *private* helper functions inside the component | If the implementation changes you’ll break tests. Instead mock the exported API functions. |
| RTL internals (e.g. `@testing-library/react`’s implementation details) | Keep the test resilient to library updates |
---
6. Integration / feature test skeleton
If you want to drive the **whole modal** end‑to‑end (e.g. using Cypress or Playwright), the tests live under `cypress/integration` (or the equivalent folder).
You’ll still **mock** the API layer, but you’ll *not* split the component into dozens of small unit tests – just test the feature as a user sees it.
---
7. Checklist for each test file
| Item | ✅ |
|------|----|
| **Imports**: `render`, `screen`, `fireEvent`, `waitFor` | |
| **Mocked APIs**: `jest.mock` the module, set `mockResolvedValue` | |
| **Prop callbacks**: `jest.fn()` for each setter that the component calls | |
| **Render**: `render( )` | |
| **Interact**: `fireEvent.change(input, { target: { value: 'foo' } })` | |
| **Assert**: `expect(createFolder).toHaveBeenCalledWith(...)` | |
| **Async wait**: `await waitFor(() => expect(...).toBeCalled())` | |
| **Clean up**: `afterEach(() => jest.clearAllMocks())` | |
| **Snapshot (optional)**: `expect(asFragment()).toMatchSnapshot()` | |
---
8. Going forward – keep the tests maintainable
- Split the component
*Pull the form out into a ` ` sub‑component* (just the ``, `
Create Project
Name
Category
{categories.map((category, index) => (
))}
setFileName("");
setIsEditFolder(false);
return;projectId: projectId,
parentId: parentId,
folderName: name,
folderId: null,const trimmedName = name.trim();if (!trimmedName) {
return;
}if (!currentProject) {
setFileName("");
setIsEditFolder(false);
return;
}const data = {
projectId: projectId,
parentId: currentProject.parentId,
folderName: trimmedName,
folderId: null,
};try {
setFileName("");
setIsEditFolder(false);
const response = await createFolder(data);
setTreeNodes(treeNodes.map((node) => (node.id === openFolder.id ? { ...node, children: [...(node.children ?? []), { id: response.data.folderId, name: trimmedName, parentId: parentId, type: "folder" }] } : node)));
//setTreeNodes(treeNodes => [...treeNodes, { id: response.data.folderId, name: trimmedName, parentId: parentId, type: "folder" }]);const newFolder = {
folderId: response.data.folderId,
folderName: trimmedName,
};setFile(null);
setFileContent(null);
setOpenFolder(null);
setIsOpenFolder(false);
} catch (error) {
console.error(error);
} finally {
setFileName("");
setIsEditFolder(false);
setIsOpenFolder(false);
}if (folderType === "folder") {
//setOpenFolder((prevOpenFolders) => ({
// ...prevOpenFolders,
// [folderId]: true,
//}));// setIsOpenFolder(true);
} else {
//const file = await getFileContent(folderId, currentProject.id);
// setOpenFolder((prevOpenFolders) => ({
// ...prevOpenFolders,
// [folderId]: true,
// }));
}const folderStructure = await getFolderStructure(projectId, null);
setTreeNodes(folderStructure);if (currentProject) {
fetchFolder();
}//if (folderName.trim().length === 0) return;
//const folderId = currentFolder.id;
//const data = {
// projectId: projectId,
// folderId: folderId,
// folderName: folderName,
//};//const response = await createFolder(data);
//setIsEditFolder(false);
//setFolderName("");// setTreeNodes(treeNodes.map((node) => (node.id === openFolder.id ? { ...node, children: [...(node.children ?? []), { id: response.data.folderId, name: folderName }] } : node)));//setTreeNodes((prevTreeNodes) => [
// ...prevTreeNodes,
// { id: response.data.folderId, name: folderName, parentId: parentId, type: "folder" },
//]);//setTreeNodes(prevTreeNodes => [...prevTreeNodes, { id: response.data.folderId, name: folderName, parentId: parentId, type: "folder" }]);e.stopPropagation();
//if (!folderName.trim()) return;
//const folderName = fileName.trim();//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}//const newFolder = { folderName };
//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}//const newFolder = { folderName };
//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}setFolderName(event.target.value);e.preventDefault();
//if (!folderName.trim()) return;
//if (!currentProject) return;
//setTreeNodes((prevTreeNodes) => [
// ...prevTreeNodes,
// { id: null, name: folderName, parentId: null, type: "folder" },
//]);//const newFolder = { folderName };
//if (currentProject) {
// const folder = await createFolder(currentProject.id, null, folderName);
// if (folder) {
// setFolders([...folders, folder]);
// }
//}Create Folder
Name
CreateFolderModal/
index.jsx
CreateFolderModal.test.jsx...setFolderName, setIsOpenFolder, …)createFolder network call, state updates)s)"/tests//*.tsx?(x)",
"**/?(*.)+(spec|test).tsx?(x)""\\.(css|less|scss)$": "identity-obj-proxy",jest.setup.js can import @testing-library/jest-dom and any global polyfills.
---
3. What to mock
| API | Why mock it | How to mock | |-----|-------------|-------------| |createFolder (your backend call) | Prevent real network traffic, control response | jest.mock('../api/folder', () => ({ createFolder: jest.fn() })) |
| getFolderStructure (used in useEffect) | Same reason | Same jest.mock trick |
| Any context/provider that the component depends on | If the component uses React Context (e.g. AuthContext, ProjectContext) | Create a dummy provider or mock the context value |
| console.error (to suppress noisy output in the test run) | Keep test logs clean | jest.spyOn(console, 'error').mockImplementation(() => {}) |
> Tip: If you’re using MSW (Mock Service Worker), you can also intercept the network calls instead of mocking the module functions. Both approaches work – pick the one you’re more comfortable with.
---
4. Writing the unit tests
Below is a minimal but fully working test file that covers the most common behaviours ofCreateFolderModal.jsx
// src/components/CreateFolderModal/CreateFolderModal.test.jsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import CreateFolderModal from './CreateFolderModal';
// 1️⃣ Mock the external API helpers
jest.mock('../../api/folder', () => ({
createFolder: jest.fn(),
}));
jest.mock('../../api/folderName', () => ({
createFolderFileName: jest.fn(),
}));
import { createFolder } from '../../api/folder';
import { createFolderFileName } from '../../api/folderName';
describe('CreateFolderModal', () => {
// Helpers – a mock “currentProject” that the modal expects
const mockProject = { id: 'proj-1', parentId: null };
const mockProps = {
currentFolder: null,
currentFolderId: null,
setCurrentFolderId: jest.fn(),
setOpenFolder: jest.fn(),
setFolderName: jest.fn(),
setIsOpenFolder: jest.fn(),
setFolderName: jest.fn(),
setCurrentFolder: jest.fn(),
setOpenFolder: jest.fn(),
setCurrentFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
setOpenFolder: jest.fn(),
// … the many callbacks you need – at least those you call
setCurrentFolder: jest.fn(),
setFolderName: jest.fn(),
setIsOpenFolder: jest.fn(),
onSubmit: jest.fn(),
};
// 2️⃣ Render once before each test – we’ll overwrite in specific tests
let utils;
beforeEach(() => {
utils = render(jest.clearAllMocks();
});
/ ------------------------------------------------------------------ /
/ 1. Renders the UI – just make sure the modal is present /
/ ------------------------------------------------------------------ /
it('renders the modal with a folder name field', () => {
expect(screen.getByLabelText(/folder name/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /create/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
});
/ ------------------------------------------------------------------ /
/ 2. Validation – empty input should not call createFolder /
/ ------------------------------------------------------------------ /
it('does not call createFolder when folder name is empty', async () => {
const createBtn = screen.getByRole('button', { name: /create/i });
fireEvent.click(createBtn);
await waitFor(() => {
expect(createFolder).not.toHaveBeenCalled();
});
});
/ ------------------------------------------------------------------ /
/ 3. Typing a folder name calls the correct API helper /
/ ------------------------------------------------------------------ /
it('calls createFolder with the right payload when a name is entered', async () => {
// Mock the API’s resolved value
createFolder.mockResolvedValue({ folderId: 'folder-123' });
// Type into the input
const input = screen.getByLabelText(/folder name/i);
fireEvent.change(input, { target: { value: 'My New Folder' } });
// Submit
fireEvent.click(screen.getByRole('button', { name: /create/i }));
// Wait for the async effect to finish
await waitFor(() => {
expect(createFolder).toHaveBeenCalledWith(
'proj-1', // currentProject.id
'My New Folder',
'proj-1' // we pass the project id again in the component
);
});
// After a successful create we expect some of the prop callbacks to run
expect(mockProps.setCurrentFolder).toHaveBeenCalled();
expect(mockProps.setOpenFolder).toHaveBeenCalled();
});
/ ------------------------------------------------------------------ /
/ 4. Cancel button resets the input field (state) and closes modal /
/ ------------------------------------------------------------------ /
it('resets state and closes the modal when Cancel is clicked', () => {
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
// Assume we typed a value first
const input = screen.getByLabelText(/folder name/i);
fireEvent.change(input, { target: { value: 'Trash' } });
// Click Cancel
fireEvent.click(cancelBtn);
// The setFolderName helper (passed as a prop) should be called with an empty string
expect(mockProps.setFolderName).toHaveBeenCalledWith('');
// The modal’s close handler should be called
expect(mockProps.setOpenFolder).toHaveBeenCalled();
});
/ ------------------------------------------------------------------ /
/ 5. Snapshot test – if the UI stays stable you can snapshot it /
/ ------------------------------------------------------------------ /
it('matches the snapshot', () => {
const { asFragment } = render(expect(asFragment()).toMatchSnapshot();
});
});
Breaking the tests into logical blocks
describe('CreateFolderModal')– a single block for the component.it('renders …')– UI rendering.it('does not call createFolder …')– validation.it('calls createFolder …')– happy‑path interaction.it('resets state on cancel')– negative path.it('matches the snapshot')– optional visual regression.
5. What *not* to test
| Implementation detail | Why avoid it | |-----------------------|--------------| | Directly asserting on `useState` variable names (`folderName` state) | Tests should care about *behaviour*, not *state shape* | | Spying on *private* helper functions inside the component | If the implementation changes you’ll break tests. Instead mock the exported API functions. | | RTL internals (e.g. `@testing-library/react`’s implementation details) | Keep the test resilient to library updates | ---6. Integration / feature test skeleton
If you want to drive the **whole modal** end‑to‑end (e.g. using Cypress or Playwright), the tests live under `cypress/integration` (or the equivalent folder). You’ll still **mock** the API layer, but you’ll *not* split the component into dozens of small unit tests – just test the feature as a user sees it. ---7. Checklist for each test file
| Item | ✅ | |------|----| | **Imports**: `render`, `screen`, `fireEvent`, `waitFor` | | | **Mocked APIs**: `jest.mock` the module, set `mockResolvedValue` | | | **Prop callbacks**: `jest.fn()` for each setter that the component calls | | | **Render**: `render(8. Going forward – keep the tests maintainable
- Split the component
No comments yet. Be the first to comment!