From 2f60be49d106e471d7d01abdea7e49ad3a4559d0 Mon Sep 17 00:00:00 2001
From: feywind <57276408+feywind@users.noreply.github.com>
Date: Mon, 31 Aug 2026 17:02:24 -0400
Subject: [PATCH] feat: add new test-utils library
---
.release-please-manifest.json | 1 +
core/test-utils/.compodocrc | 10 +
core/test-utils/.gitattributes | 4 +
core/test-utils/.gitignore | 15 ++
core/test-utils/.repo-metadata.json | 11 ++
core/test-utils/CHANGELOG.md | 5 +
core/test-utils/CODE_OF_CONDUCT.md | 94 ++++++++++
core/test-utils/CONTRIBUTING.md | 68 +++++++
core/test-utils/LICENSE | 202 +++++++++++++++++++++
core/test-utils/README.md | 59 ++++++
core/test-utils/jest.config.js | 21 +++
core/test-utils/package.json | 52 ++++++
core/test-utils/src/index.ts | 20 ++
core/test-utils/src/testResources.ts | 179 ++++++++++++++++++
core/test-utils/test/testResources.test.ts | 127 +++++++++++++
core/test-utils/tsconfig.json | 11 ++
release-please-config.json | 3 +
17 files changed, 882 insertions(+)
create mode 100644 core/test-utils/.compodocrc
create mode 100644 core/test-utils/.gitattributes
create mode 100644 core/test-utils/.gitignore
create mode 100644 core/test-utils/.repo-metadata.json
create mode 100644 core/test-utils/CHANGELOG.md
create mode 100644 core/test-utils/CODE_OF_CONDUCT.md
create mode 100644 core/test-utils/CONTRIBUTING.md
create mode 100644 core/test-utils/LICENSE
create mode 100644 core/test-utils/README.md
create mode 100644 core/test-utils/jest.config.js
create mode 100644 core/test-utils/package.json
create mode 100644 core/test-utils/src/index.ts
create mode 100644 core/test-utils/src/testResources.ts
create mode 100644 core/test-utils/test/testResources.test.ts
create mode 100644 core/test-utils/tsconfig.json
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 941a71b15762..5663e257e4bb 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -21,6 +21,7 @@
"core/precise-date": "6.0.1",
"core/projectify": "6.0.1",
"core/promisify": "6.0.1",
+ "core/test-utils": "0.1.0",
"handwritten/bigquery": "9.0.3",
"handwritten/bigquery-storage": "6.0.1",
"handwritten/bigtable": "7.2.0",
diff --git a/core/test-utils/.compodocrc b/core/test-utils/.compodocrc
new file mode 100644
index 000000000000..cd8b42152a61
--- /dev/null
+++ b/core/test-utils/.compodocrc
@@ -0,0 +1,10 @@
+---
+tsconfig: ./tsconfig.json
+output: ./docs
+theme: material
+hideGenerator: true
+disablePrivate: true
+disableProtected: true
+disableInternal: true
+disableCoverage: true
+disableGraph: true
diff --git a/core/test-utils/.gitattributes b/core/test-utils/.gitattributes
new file mode 100644
index 000000000000..33739cb74e44
--- /dev/null
+++ b/core/test-utils/.gitattributes
@@ -0,0 +1,4 @@
+*.ts text eol=lf
+*.js text eol=lf
+protos/* linguist-generated
+**/api-extractor.json linguist-language=JSON-with-Comments
diff --git a/core/test-utils/.gitignore b/core/test-utils/.gitignore
new file mode 100644
index 000000000000..4b8206885b0f
--- /dev/null
+++ b/core/test-utils/.gitignore
@@ -0,0 +1,15 @@
+**/*.log
+**/node_modules
+.coverage
+coverage/
+.nyc_output
+docs/
+out/
+system-test/secrets.js
+system-test/*key.json
+*.lock
+*-lock.js*
+build/
+.vscode
+package-lock.json
+__pycache__
diff --git a/core/test-utils/.repo-metadata.json b/core/test-utils/.repo-metadata.json
new file mode 100644
index 000000000000..daa524307f6c
--- /dev/null
+++ b/core/test-utils/.repo-metadata.json
@@ -0,0 +1,11 @@
+{
+ "name": "test-utils",
+ "name_pretty": "Google Test Utilities",
+ "release_level": "preview",
+ "language": "nodejs",
+ "repo": "googleapis/google-cloud-node",
+ "distribution_name": "google-test-utils",
+ "client_documentation": "https://cloud.google.com/nodejs/docs/reference/test-utils/latest",
+ "library_type": "OTHER",
+ "codeowner_team": "jsteam-handwritten-libraries"
+}
diff --git a/core/test-utils/CHANGELOG.md b/core/test-utils/CHANGELOG.md
new file mode 100644
index 000000000000..8a3d7478515c
--- /dev/null
+++ b/core/test-utils/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+[npm history][1]
+
+[1]: https://www.npmjs.com/package/google-test-utils?activeTab=versions
diff --git a/core/test-utils/CODE_OF_CONDUCT.md b/core/test-utils/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000000..a044123a3f2f
--- /dev/null
+++ b/core/test-utils/CODE_OF_CONDUCT.md
@@ -0,0 +1,94 @@
+
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to *googleapis-stewards@google.com*, the
+Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out to the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
diff --git a/core/test-utils/CONTRIBUTING.md b/core/test-utils/CONTRIBUTING.md
new file mode 100644
index 000000000000..b9aca5ba8c7e
--- /dev/null
+++ b/core/test-utils/CONTRIBUTING.md
@@ -0,0 +1,68 @@
+# How to become a contributor and submit your own code
+
+**Table of contents**
+
+* [Contributor License Agreements](#contributor-license-agreements)
+* [Contributing a patch](#contributing-a-patch)
+* [Running the tests](#running-the-tests)
+* [Releasing the library](#releasing-the-library)
+
+## Contributor License Agreements
+
+We'd love to accept your sample apps and patches! Before we can take them, we
+have to jump a couple of legal hurdles.
+
+Please fill out either the individual or corporate Contributor License Agreement
+(CLA).
+
+ * If you are an individual writing original source code and you're sure you
+ own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual).
+ * If you work for a company that wants to allow you to contribute your work,
+ then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate).
+
+Follow either of the two links above to access the appropriate CLA and
+instructions for how to sign and return it. Once we receive it, we'll be able to
+accept your pull requests.
+
+## Contributing A Patch
+
+1. Submit an issue describing your proposed change to the repo in question.
+1. The repo owner will respond to your issue promptly.
+1. If your proposed change is accepted, and you haven't already done so, sign a
+ Contributor License Agreement (see details above).
+1. Fork the desired repo, develop and test your code changes.
+1. Ensure that your code adheres to the existing style in the code to which
+ you are contributing.
+1. Ensure that your code has an appropriate set of tests which all pass.
+1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling.
+1. Submit a pull request.
+
+### Before you begin
+
+1. [Select or create a Cloud Platform project][projects].
+1. [Set up authentication with a service account][auth] so you can access the
+ API from your local workstation.
+
+
+## Running the tests
+
+1. [Prepare your environment for Node.js setup][setup].
+
+1. Install dependencies:
+
+ npm install
+
+1. Run the tests:
+
+ # Run unit tests.
+ npm test
+
+1. Lint (and maybe fix) any changes:
+
+ npm run fix
+
+[setup]: https://cloud.google.com/nodejs/docs/setup
+[projects]: https://console.cloud.google.com/project
+[billing]: https://support.google.com/cloud/answer/6293499#enable-billing
+
+[auth]: https://cloud.google.com/docs/authentication/getting-started
diff --git a/core/test-utils/LICENSE b/core/test-utils/LICENSE
new file mode 100644
index 000000000000..d64569567334
--- /dev/null
+++ b/core/test-utils/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/core/test-utils/README.md b/core/test-utils/README.md
new file mode 100644
index 000000000000..4f88b412e236
--- /dev/null
+++ b/core/test-utils/README.md
@@ -0,0 +1,59 @@
+
+
+# [Google Test Utilities: Node.js](https://github.com/googleapis/google-cloud-node)
+
+[](https://cloud.google.com/terms/launch-stages)
+[](https://www.npmjs.org/package/google-test-utils)
+
+Testing utilities for Google Cloud client libraries.
+
+A comprehensive list of changes in each version may be found in
+[the CHANGELOG](https://github.com/googleapis/google-cloud-node/blob/main/core/test-utils/CHANGELOG.md).
+
+* [github.com/googleapis/google-cloud-node](https://github.com/googleapis/google-cloud-node)
+
+**Table of contents:**
+
+* [Quickstart](#quickstart)
+ * [Installing the library](#installing-the-library)
+ * [Using the library](#using-the-library)
+* [Versioning](#versioning)
+* [Contributing](#contributing)
+* [License](#license)
+
+## Quickstart
+
+### Installing the library
+
+```bash
+npm install google-test-utils
+```
+
+It's unlikely you will need to install this package directly, as it is intended as an internal testing utility across Google Cloud Node.js client libraries.
+
+### Using the library
+
+```typescript
+import {TestResources} from 'google-test-utils';
+
+const testResources = new TestResources('my-test-suite');
+const resourceName = testResources.generateName('my-test');
+```
+
+## Supported Node.js Versions
+
+This library is not publicly supported.
+
+## Versioning
+
+This library follows [Semantic Versioning](http://semver.org/).
+
+## Contributing
+
+Contributions welcome! See the [Contributing Guide](https://github.com/googleapis/google-cloud-node/blob/main/core/test-utils/CONTRIBUTING.md).
+
+## License
+
+Apache Version 2.0
+
+See [LICENSE](https://github.com/googleapis/google-cloud-node/blob/main/core/test-utils/LICENSE)
diff --git a/core/test-utils/jest.config.js b/core/test-utils/jest.config.js
new file mode 100644
index 000000000000..2a860f423405
--- /dev/null
+++ b/core/test-utils/jest.config.js
@@ -0,0 +1,21 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+module.exports = {
+ testMatch: ['/test/**/*.ts'],
+ transform: {
+ '^.+\\.tsx?$': ['ts-jest', {tsconfig: 'tsconfig.json'}],
+ },
+ clearMocks: true,
+};
diff --git a/core/test-utils/package.json b/core/test-utils/package.json
new file mode 100644
index 000000000000..7b26072b653b
--- /dev/null
+++ b/core/test-utils/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "google-test-utils",
+ "version": "0.1.0",
+ "description": "Testing utilities for Google Cloud client libraries",
+ "main": "build/src/index.js",
+ "types": "build/src/index.d.ts",
+ "repository": {
+ "type": "git",
+ "directory": "core/test-utils",
+ "url": "https://github.com/googleapis/google-cloud-node.git"
+ },
+ "scripts": {
+ "test": "jest --coverage",
+ "compile": "tsc -p .",
+ "fix": "gts fix",
+ "lint": "gts check",
+ "prepare": "npm run compile",
+ "pretest": "npm run compile",
+ "docs": "jsdoc -c .jsdoc.js",
+ "samples-test": "echo 'no samples test'",
+ "system-test": "echo 'no system test'",
+ "clean": "gts clean",
+ "precompile": "gts clean"
+ },
+ "keywords": [
+ "google",
+ "testing",
+ "test",
+ "utilities"
+ ],
+ "files": [
+ "build/src",
+ "!build/src/**/*.map"
+ ],
+ "author": "Google Inc.",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@types/jest": "^29.5.12",
+ "@types/node": "^22.13.8",
+ "gts": "^6.0.2",
+ "jest": "^29.7.0",
+ "jsdoc": "^4.0.4",
+ "jsdoc-fresh": "^6.0.0",
+ "jsdoc-region-tag": "^5.0.0",
+ "ts-jest": "^29.4.10",
+ "typescript": "^5.8.2"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "homepage": "https://github.com/googleapis/google-cloud-node/tree/main/core/test-utils"
+}
diff --git a/core/test-utils/src/index.ts b/core/test-utils/src/index.ts
new file mode 100644
index 000000000000..f2e68c1091cd
--- /dev/null
+++ b/core/test-utils/src/index.ts
@@ -0,0 +1,20 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export {
+ Resource,
+ TestResources,
+ TokenMaker,
+ defaultMaker as defaultTokenMaker,
+} from './testResources';
diff --git a/core/test-utils/src/testResources.ts b/core/test-utils/src/testResources.ts
new file mode 100644
index 000000000000..440c4f1d1b35
--- /dev/null
+++ b/core/test-utils/src/testResources.ts
@@ -0,0 +1,179 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as crypto from 'node:crypto';
+
+// Returns a shortened UUID that can be used to identify a
+// specific run of a specific test.
+function shortUUID(): string {
+ return crypto.randomUUID().split('-').shift()!;
+}
+
+export interface TokenMaker {
+ uuid(): string;
+ timestamp(): number;
+}
+
+export const defaultMaker: TokenMaker = {
+ uuid: shortUUID,
+ timestamp: () => Date.now(),
+};
+
+export interface Resource {
+ name?: string | null | undefined;
+ delete?(): Promise;
+}
+
+function normalizeId(id: string): string {
+ return id.replace(/-/g, '_');
+}
+
+/**
+ * Manages the names of testing resources during a test run. It's
+ * easy to accidentally leak resources, and it's easy to accidentally
+ * have conflicts with tests running concurrently, so this class helps
+ * you manage them.
+ *
+ * Used nomenclature:
+ * Test - a single test for a single aspect of code; for example,
+ * "create a topic in pub/sub"
+ * Test Suite - a collection of tests that are generally run together;
+ * for example, "test topic operations in pub/sub"
+ * Test Run - a single run of a test suite (or single test within a suite);
+ * for example, "run the tests for PR #1234, 5th attempt"
+ */
+export class TestResources {
+ testSuiteId: string;
+ currentTime: string;
+ tokenMaker: TokenMaker;
+
+ /**
+ * @param testSuiteId [string] A unique ID for a test suite (e.g.
+ * pubsub-topics).
+ * @param tokenMaker [TokenMaker] Optional token generator for IDs and timestamps.
+ */
+ constructor(testSuiteId: string, tokenMaker: TokenMaker = defaultMaker) {
+ this.testSuiteId = normalizeId(testSuiteId);
+ this.currentTime = `${tokenMaker.timestamp()}`;
+ this.tokenMaker = tokenMaker;
+ }
+
+ /**
+ * Returns the resource prefix for the current run of the test suite.
+ * Optionally, testId may specify the specific ID of a test in the
+ * suite.
+ */
+ getPrefix(testId?: string): string {
+ if (testId) {
+ return [this.testSuiteId, this.currentTime, normalizeId(testId)].join(
+ '-',
+ );
+ } else {
+ return [this.testSuiteId, this.currentTime].join('-');
+ }
+ }
+
+ /**
+ * Generates a unique resource name for one run of a test within
+ * a test suite.
+ */
+ generateName(testId: string): string {
+ return [this.getPrefix(testId), this.tokenMaker.uuid()].join('-');
+ }
+
+ /**
+ * Generates a unique resource name for one run of a test within
+ * a test suite for BigQuery resources.
+ */
+ generateBigQueryName(testId: string): string {
+ return [normalizeId(this.getPrefix(testId)), this.tokenMaker.uuid()].join(
+ '_',
+ );
+ }
+
+ /**
+ * Generates a unique resource name for one run of a test within
+ * a test suite for Cloud Storage resources.
+ */
+ generateStorageName(testId: string): string {
+ return [normalizeId(this.getPrefix(testId)), this.tokenMaker.uuid()].join(
+ '_',
+ );
+ }
+
+ /**
+ * Given a list of resource names (and a test ID), this will return
+ * a list of all resources that should be deleted to clean up for
+ * the current run of that particular test.
+ */
+ filterForTest(testId: string, allResources: Resource[]): Resource[] {
+ const prefix = this.getPrefix(testId);
+ return allResources.filter(n => n.name?.includes(prefix));
+ }
+
+ /**
+ * Given a list of resource names, this will return a list of all
+ * resources that should be deleted to clean up after the current
+ * run of a test suite.
+ */
+ filterForCurrentRun(allResources: Resource[]): Resource[] {
+ const prefix = this.getPrefix();
+ return allResources.filter(n => n.name?.includes(prefix));
+ }
+
+ /**
+ * Given a list of resource names, this will return a list of all
+ * resources that should be deleted to clean up after any run
+ * of the current test suite. Note that some of the names may
+ * still be in use.
+ */
+ filterForSuite(allResources: Resource[]): Resource[] {
+ return allResources.filter(n => n.name?.includes(this.testSuiteId));
+ }
+
+ /**
+ * Given a list of resource names, this will return a list of all
+ * resources that should be deleted to generally clean up after any
+ * run of the current test suite. This is much like filterForSuite(),
+ * but it also filters by age - items that are less than 2 hours
+ * old will not be cleaned.
+ */
+ filterForCleanup(allResources: Resource[]): Resource[] {
+ const currentRunPrefix = this.getPrefix();
+ return allResources.filter(n => {
+ let name = n.name || undefined;
+ if (name === undefined) {
+ return false;
+ }
+
+ // We'll always get at least one thing.
+ name = name.split('/').pop()!;
+
+ if (name.startsWith(currentRunPrefix)) {
+ return true;
+ }
+
+ if (name.startsWith(this.testSuiteId)) {
+ const parts = name.split('-');
+ const createdAt = Number(parts[1]);
+ const timeDiff = (Date.now() - createdAt) / (1000 * 60 * 60);
+ if (timeDiff >= 2) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+ }
+}
diff --git a/core/test-utils/test/testResources.test.ts b/core/test-utils/test/testResources.test.ts
new file mode 100644
index 000000000000..5e95fc021257
--- /dev/null
+++ b/core/test-utils/test/testResources.test.ts
@@ -0,0 +1,127 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {TestResources, TokenMaker, Resource, defaultTokenMaker} from '../src';
+
+describe('testResources (unit)', () => {
+ const fixedId = 'fixed';
+ const fixedTime = Date.now();
+ const fakeTokenMaker: TokenMaker = {
+ uuid: () => fixedId,
+ timestamp: () => fixedTime,
+ };
+
+ const suiteId = 'someSuite';
+ let testResources: TestResources;
+
+ beforeEach(() => {
+ testResources = new TestResources(suiteId, fakeTokenMaker);
+ });
+
+ it('has predictable prefixes', () => {
+ const prefix = testResources.getPrefix('testId');
+ expect(prefix).toBe(`${suiteId}-${fixedTime}-testId`);
+
+ const normalizedPrefix = testResources.getPrefix('test-id-dashes');
+ expect(normalizedPrefix).toBe(`${suiteId}-${fixedTime}-test_id_dashes`);
+
+ const suitePrefix = testResources.getPrefix();
+ expect(suitePrefix).toBe(`${suiteId}-${fixedTime}`);
+ });
+
+ it('generates names', () => {
+ const prefix = testResources.getPrefix('testId');
+ const name = testResources.generateName('testId');
+ expect(name).toBe(`${prefix}-${fixedId}`);
+ });
+
+ it('generates BigQuery names', () => {
+ const name = testResources.generateBigQueryName('testId');
+ expect(name).toBe(`${suiteId}_${fixedTime}_testId_${fixedId}`);
+ });
+
+ it('generates Storage names', () => {
+ const name = testResources.generateStorageName('testId');
+ expect(name).toBe(`${suiteId}_${fixedTime}_testId_${fixedId}`);
+ });
+
+ it('filters for test', () => {
+ const resources: Resource[] = [
+ {name: `${suiteId}-${fixedTime}-testA-123`},
+ {name: `${suiteId}-${fixedTime}-testB-456`},
+ {name: 'unrelated'},
+ ];
+ const filtered = testResources.filterForTest('testA', resources);
+ expect(filtered).toEqual([{name: `${suiteId}-${fixedTime}-testA-123`}]);
+ });
+
+ it('filters for current run', () => {
+ const resources: Resource[] = [
+ {name: `${suiteId}-${fixedTime}-testA-123`},
+ {name: `${suiteId}-${fixedTime - 1000}-testA-456`},
+ {name: 'unrelated'},
+ ];
+ const filtered = testResources.filterForCurrentRun(resources);
+ expect(filtered).toEqual([{name: `${suiteId}-${fixedTime}-testA-123`}]);
+ });
+
+ it('filters for suite', () => {
+ const resources: Resource[] = [
+ {name: `${suiteId}-${fixedTime}-testA-123`},
+ {name: `${suiteId}-${fixedTime - 1000}-testB-456`},
+ {name: 'unrelated'},
+ ];
+ const filtered = testResources.filterForSuite(resources);
+ expect(filtered).toEqual([
+ {name: `${suiteId}-${fixedTime}-testA-123`},
+ {name: `${suiteId}-${fixedTime - 1000}-testB-456`},
+ ]);
+ });
+
+ it('filters for cleanup', () => {
+ const resources: Resource[] = [
+ {
+ // Not related
+ name: 'ooga',
+ },
+ {
+ // Resource without name
+ name: undefined,
+ },
+ {
+ // For current test run
+ name: `${suiteId}-${fixedTime}-bob-98719284791`,
+ },
+ {
+ // For previous test run, but not very old
+ name: `${suiteId}-${fixedTime - 100}-bob-124897912`,
+ },
+ {
+ // For previous test run, but old (> 2 hours)
+ name: `${suiteId}-${fixedTime - 3000 * 60 * 60}-bob-57823975`,
+ },
+ ];
+ const filtered = testResources.filterForCleanup(resources);
+ expect(filtered.length).toBe(2);
+ expect(filtered.filter(r => r.name?.includes('bob-9871')).length).toBe(1);
+ expect(filtered.filter(r => r.name?.includes('bob-5782')).length).toBe(1);
+ });
+
+ it('supports default maker', () => {
+ const defaultResources = new TestResources('default-suite');
+ expect(defaultResources.testSuiteId).toBe('default_suite');
+ expect(typeof defaultTokenMaker.uuid()).toBe('string');
+ expect(typeof defaultTokenMaker.timestamp()).toBe('number');
+ });
+});
diff --git a/core/test-utils/tsconfig.json b/core/test-utils/tsconfig.json
new file mode 100644
index 000000000000..26f33cd1f4d3
--- /dev/null
+++ b/core/test-utils/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./node_modules/gts/tsconfig-google.json",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "build"
+ },
+ "include": [
+ "src/*.ts",
+ "test/*.ts"
+ ]
+}
diff --git a/release-please-config.json b/release-please-config.json
index a8723c83e58e..8e8093cf3155 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -69,6 +69,9 @@
"core/promisify": {
"component": "promisify"
},
+ "core/test-utils": {
+ "component": "test-utils"
+ },
"handwritten/bigquery": {
"component": "bigquery"
},