Anyone who is a Software Engineer familiar with .NET
uses NuGet packages. The bigger the solution, the more chaos there is.
In this article, I will explain how to test NuGet packages for the whole solution and describe several real-life cases where it can be useful. And yes, we'll be writing code.
The solution preparing
Before considering different cases, let's create the base solution.
Choose a location on your PC and create a folder where the solution will be placed. Then, go to this folder at once.
mkdir PackageTestSample
cd PackageTestSample
Next, you need to create the empty solution in a previously created folder:
dotnet new sln -n PackageTestSample
Now, I'll create three projects to demonstrate clearly how it works:
- Create a web API project from a template. Any code there doesn't matter. Remember to add the project to the solution.
dotnet new webapi -n PackageTestSample.AppSample --no-openapi
dotnet sln add PackageTestSample.AppSample/PackageTestSample.AppSample.csproj
cd PackageTestSample.AppSample
For testing, let's add the package at once.
dotnet add package Swashbuckle.AspNetCore --version 6.6.2
- Create a library project from a template. We won't add the package yet, and the code doesn't matter.
dotnet new classlib -n PackageTestSample.Domain
dotnet sln add PackageTestSample.Domain/PackageTestSample.Domain.csproj
- Create a test project from the template that contains the xUnit framework. We'll run only this project.
dotnet new xunit -n PackageTestSample.UnitTests
dotnet sln add PackageTestSample.UnitTests/PackageTestSample.UnitTests.csproj
cd PackageTestSample.UnitTests
We need access to the NuGet API to test NuGet packages, so we should install an additional package.
dotnet add package NuGet.Protocol --version 6.12.1
Tests preparing
We can prepare test infrastructure when all projects are created, and all packages are added.
Go to the PackageTestSample.UnitTests and create Models folder:
cd PackageTestSample.UnitTests
mkdir Models
cd Models
Let's create four models that cover crucial package criteria: general information, license information, deprecation, and vulnerability information. Sure, you can join all these models into one, but I split them for the best clarity.
dotnet new class -n PackageInfo
dotnet new class -n PackageLicenseInfo
dotnet new class -n PackageVulnerabilityInfo
dotnet new class -n PackageDeprecationInfo
Please modify these classes:
public class PackageInfo
{
public required string Project { get; set; }
public required string NuGetPackage { get; set; }
public required string Version { get; set; }
}
public class PackageLicenseInfo : PackageInfo
{
public required string License { get; set; }
}
public class PackageVulnerabilityInfo : PackageInfo
{
public required IEnumerable<PackageVulnerabilityMetadata> Vulnerabilities { get; set; }
}
public class PackageDeprecationInfo : PackageInfo
{
public bool IsDeprecated { get; set; }
}
Next, please create a new class where we'll write tests.
dotnet new class -n NuGetPackageTests
Modify this class and add this code:
public class NuGetPackageTests
{
private static string[] _projectFiles;
private static string _solutionDirectory;
public NuGetPackageTests()
{
_solutionDirectory = Directory.GetCurrentDirectory()
.Split("PackageTestSample.UnitTests")
.First();
_projectFiles = ListSolutionProjectPaths();
}
private static string[] ListSolutionProjectPaths()
{
return Directory.GetFiles(
path: _solutionDirectory,
searchPattern: "*.csproj",
searchOption: SearchOption.AllDirectories
);
}
}
We define the solution's directory in the constructor and get all existing project paths.
Testing
Now, let's consider crucial cases for necessity test NuGet packages.
Case 1: You need only free or specific licenses.
Since most projects are created for commercial use, you need to control licenses. Some packages can contain restrictions. You can test that your solution has only suited licenses.
Let's add the first test, which tests desired licenses:
[Fact]
public async Task CheckPackageLicenses()
{
//Arrange
var restrictedNuGetPackages = new List<PackageLicenseInfo>();
var allowedNugetPackageLicenses = new List<string>
{
"MIT",
"Apache-2.0",
"Microsoft"
};
var resource = await GetNugetResourceAsync();
//Act
foreach (var projectFile in _projectFiles)
{
foreach (var packageReference in ListNuGetPackages(projectFile))
{
var metadata = await GetNuGetMetadataAsync(packageReference, resource);
var licenseMetadata = metadata.LicenseMetadata?.License ?? metadata.Authors;
if (allowedNugetPackageLicenses.Contains(licenseMetadata)) continue;
var nugetPackage = new PackageLicenseInfo
{
NuGetPackage = packageReference.NuGetPackage,
Version = packageReference.Version,
Project = packageReference.Project,
License = licenseMetadata
};
restrictedNuGetPackages.Add(nugetPackage);
}
}
//Assert
Assert.Empty(restrictedNuGetPackages);
}
This test has errors since we should get NuGet packages from the server.
private async Task<PackageMetadataResource> GetNugetResourceAsync()
{
var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json");
return await repository.GetResourceAsync<PackageMetadataResource>();
}
You still have issues. You should get all installed packages from each project. Please, add this method:
private static PackageInfo[] ListNuGetPackages(string projectFilePath)
{
return XDocument
.Load(projectFilePath)
.Descendants("PackageReference")
.Select(packageReference=> new PackageInfo
{
Project = projectFilePath.Split('\\').Last().Split(".csproj").First(),
NuGetPackage = packageReference.Attribute("Include")?.Value ?? string.Empty,
Version = packageReference.Attribute("Version")?.Value ?? string.Empty
}).ToArray();
}
It still isn't working. Since, your project doesn't contain information about license. You need to get package's metadata.
private async Task<IPackageSearchMetadata> GetNuGetMetadataAsync(PackageInfo packageReference, PackageMetadataResource resource)
{
var packageIdentity = new PackageIdentity(
id: packageReference.NuGetPackage,
version: NuGetVersion.Parse(packageReference.Version)
);
return await resource.GetMetadataAsync(
package: packageIdentity,
sourceCacheContext: new SourceCacheContext(),
log: NullLogger.Instance,
token: default
);
}
Let's check it. Go to the PackageTestSample.UnitTests
project and run tests:
cd PackageTestSample.UnitTests
dotnet test
The test should be successful.
I won't write a negative test. I just will show this case. If you add a package with a different license like this:
dotnet add package Syncfusion.Licensing --version 27.2.5
As was expected, the test got a fail.
Case 2: You don't want to use packages with vulnerabilities.
There is a grave reason to check the NuGet packages. A package can become vulnerable later, decreasing your security. You may not even know about it. The test will help you with this.
[Fact]
public async Task CheckPackageVulnerabilities()
{
// Arrange
var vulnerableNuGetPackages = new List<PackageVulnerabilityInfo>();
var resource = await GetNugetResourceAsync();
// Act
foreach (var projectFile in _projectFiles)
{
foreach (var packageReference in ListNuGetPackages(projectFile))
{
var metadata = await GetNuGetMetadataAsync(packageReference, resource);
var vulnerabilities = metadata.Vulnerabilities ?? new List<PackageVulnerabilityMetadata>();
if (!vulnerabilities.Any()) continue;
var nugetPackage = new PackageVulnerabilityInfo
{
NuGetPackage = packageReference.NuGetPackage,
Version = packageReference.Version,
Project = packageReference.Project,
Vulnerabilities = vulnerabilities
};
vulnerableNuGetPackages.Add(nugetPackage);
}
}
// Assert
Assert.Empty(vulnerableNuGetPackages);
}
Information about vulnerabilities can be obtained from metadata.
Check this out. With packages, everything is OK.
Now, let's check how it works with existing vulnerabilities. Please, add this package:
dotnet add package Newtonsoft.Json --version 12.0.3
Let's check. How it was expected, we got the fail.
Case 3: You don't want to use deprecated packages.
This test is similar to the previous one. The deprecated packages pose little danger. However, they are potentially vulnerable packages, and you can't update them. You need to add a new package and change the code. The best solution is to control these packages and change them as soon as possible.
[Fact]
public async Task CheckDeprecatedPackage()
{
// Arrange
var deprecatedPackages = new List<PackageDeprecationInfo>();
var resource = await GetNugetResourceAsync();
// Act
foreach (var projectFile in _projectFiles)
{
foreach (var packageReference in ListNuGetPackages(projectFile))
{
var metadata = await GetNuGetMetadataAsync(packageReference, resource);
var tags = metadata.Tags ?? string.Empty;
if (!tags.Contains("Deprecated")) continue;
var nugetPackage = new PackageDeprecationInfo
{
NuGetPackage = packageReference.NuGetPackage,
Version = packageReference.Version,
Project = packageReference.Project,
IsDeprecated = (await metadata.GetDeprecationMetadataAsync()).Reasons.Any()
};
deprecatedPackages.Add(nugetPackage);
}
}
// Assert
Assert.Empty(deprecatedPackages);
}
You can check this.
But if you add an actual deprecated package, you'll get a fail.
dotnet add package EntityFramework.MappingAPI --version 6.2.1
Case 4: You have different versions of the same package
I think you have faced a situation where the solution has the same package in different projects but not identical versions. The IDE will throw warnings, but the test will prompt you to fix it. You also can check it. Add this code:
[Fact]
public void CheckPackageVersionMismatches()
{
// Arrange
var installedNuGetPackages = new List<PackageInfo>();
foreach (var projectFile in _projectFiles)
{
installedNuGetPackages.AddRange(ListNuGetPackages(projectFile));
}
// Act
var packagesToConsolidate = installedNuGetPackages
.GroupBy(package => package.NuGetPackage)
.Where(packageGroup => packageGroup.Select(package => package.Version).Distinct().Count() > 1)
.Select(packageToConsolidate => new
{
PackageName = packageToConsolidate.Key,
Versions = packageToConsolidate.Select(package => $"{package.Project}: {package.Version}")
}).ToList();
// Assert
Assert.Empty(packagesToConsolidate);
}
Please, check it.
But if I install the package with a higher version to another project, I'll get a fail.
dotnet add package Swashbuckle.AspNetCore --version 7.1.0
Case 5: You want to restrict installing packages.
In this case, the project should stay empty when you do not need any packages. Any developer can add packages by mistake. It would be best if you do not have this issue. Not-used packages require additional memory allocation. In another case, you can control specific packages.
[Fact]
public void CheckNoInstalledNuGetPackages()
{
//Arrange & Act
var projectFiles = Directory.GetFiles(_solutionDirectory, "*Domain.csproj", SearchOption.AllDirectories);
var packages = new List<PackageInfo>();
foreach (var projectFile in projectFiles)
{
packages.AddRange(ListNuGetPackages(projectFile).ToList());
}
//Assert
Assert.Empty(packages);
}
Please check it again.
Install this package for the domain empty project.
dotnet add package Swashbuckle.AspNetCore --version 6.6.2
We got failure.
Case 6: You need a specific version of the package.
Imagine the situation when you don't update the package because it can influence the entire solution. Or the newest version requires sufficient code changes, and you aren't ready to make them. The test helps you control specific versions against updating.
[Fact]
public void CheckAllowedVersionsNuGetPackages()
{
//Arrange & Act
var projectFiles = Directory.GetFiles(_solutionDirectory, "*AppSample.csproj", SearchOption.AllDirectories);
var packages = new List<PackageInfo>();
foreach (var projectFile in projectFiles)
{
packages.AddRange(ListNuGetPackages(projectFile).ToList());
}
//Assert
Assert.Equal("6.6.2", packages.FirstOrDefault(p => p.NuGetPackage == "Swashbuckle.AspNetCore")?.Version);
}
Let's run it.
And now, let's update Swashbuckle.AspNetCore
package.
Conclutions
It's an awesome tool for controlling NuGet packages. You can test specific packages, versions, licenses, and vulnerabilities. It's super easy to use to create tests. I hope this topic was helpful to you. See you later. Happy coding!
Source code: LINK
Top comments (0)