Why Developers Don’t Write Unit Tests

When I started my career in software development 22 years ago this year, it seemed that unit testing adoption was rather patchy. Whether you wrote unit tests or not, depended upon the environment you worked in. So most developers only used to write unit tests if they worked on a project which mandated unit testing, for example working on a safety critical system or defence project. Or if they worked within a team with people who saw the merits of writing unit tests.

Unit testing seemed to become steadily more popular in the 2000s. I worked on a few projects early on in my career that advocated the use of unit tests. This may have been using a specific tool or creating specific projects to exercise the code that you have written. I remember my team lead at a job that I had in 2005, came in one morning before we started work and demonstrated NUnit to me. I thought this was great and I think it influenced my thinking going forward.

Later with the advent of continuous integration becoming popular in software development, software developers started to use open source unit test frameworks like NUnit. I found that the more places I worked, more people were adopting unit testing as part of their development process. But I still find that software developers don’t write unit tests. So, this article attempts to explore the reasons why developers don’t write unit tests.

You Don’t Know Any Better

You might be a junior developer fresh out of university and may have heard of unit testing at university, but not embraced the benefits of writing unit tests. Never finding the time to be patient with yourself to read up on unit testing and experiment with a framework. You know there must be a better way, but don’t know what it is.

You Are Set In Your Ways

You might have been writing software for years and do your own developer testing and produce robust code. You might then think, what I am doing works for me, why learn another technique?

The Testers Will Find The Bugs

Some developers don’t see it as their responsibility to test the code that they write. They see it as the responsibility of the test team to find the bugs and report the bugs to them to be fixed. The only trouble with this approach is, you will probably end up delivering code which simply does not work. Imagine being the unfortunate testers, picking up the release only to find that it crashes and burns with runs of their test scenarios?

Nobody Sees The Value Of Unit Testing In Your Team

You work in a team that sees writing unit tests as a waste of developers time. There is already a test team in place which runs manual tests against the software release, so why do we need to spend extra effort writing unit tests? There is an attitude of “this is great, but we don’t really have time to do this”. So any efforts to introduce unit testing meet a lot of resistance.

You Inherited A Legacy Code base With No Unit Tests

You are maintaining a legacy application which is still critical for business, but has no unit tests. There is very little in the way of documentation and most of how it is supposed to work is held in a few people’s heads. The software release relies heavily on manual testing which does seem to work, but you still end up with a number of issues after each release. The code base is tightly coupled and doesn’t really lend itself to unit testing. There is no budget left to refactor the code to make it testable, again not seeing the value of investing the time an effort in doing this.

No Incentive To Write Unit Tests

The culture of your development team pays lip service to unit testing. There ends up being a task to write unit tests, but it ends up not being done because of tight delivery timescales. Or if any unit tests are written, they end up being low value tests which instantiate objects and do little more than that. There is a greater emphasis on delivering the code in the time allocated for that piece of work and then moving on to the next piece of work.

The developers seen to be cutting the most code or fixing the most bugs are rewarded. So we end up with a code base which is difficult to change when it goes into production. People have sleepless nights worrying if the change they made may have broken something unintentionally somewhere else. Changing the code ends up like playing a game of Kerplunk.

You Are A Code Firefighter

You have a reputation of solving problems which are of high urgency. There is pressure to get the code fixed and deployed to production as soon as possible. The trouble is, this is often done at the expense of good programming practices. The problem gets fixed or fixed for now, but people feel fearful of changing the code in the future, for fear of re-introducing the problem.

Conclusion

You may identify with some of these reasons for not writing unit tests now or in the past. You may think that these are valid reasons for not writing unit tests. But I think you are missing an opportunity to up-skill as a programmer and make your life as a developer easier and more rewarding by embracing unit testing.

So where do you start your unit testing journey? I have worked with people who did not write unit tests and have embraced writing them. One notable comment from a colleague from my past, which sticks with me is “I can’t imagine writing code without unit tests now”.

  • You could start by reading this book or the previous editions: The Art Of Unit Testing
  • You could try out the examples on this page: Getting Started With Unit Testing
  • Find someone you know who already does unit testing and ask them to help you get started. From my experience people respond well to enthusiasm

What is Google Glass?

So back in June 2014, I was invited to try out Google Glass at an event in London. So I made an appointment to go after work on a Friday evening, which worked out rather well because the trains were delayed from Waterloo that day. So I headed off to St Pancras station to find out “What is Google Glass”.

So I arrived at the venue and gave my details, they then promptly stamped my hand.

HandStamp
I was stamped on entry

Fitting

I was fitted with Google Glass and was directed to make my way around the stands to try out the different features. First of all they gave you a chance to try out the basic operation. This starts with saying “Ok Glass”, and next the command you want to execute like “Take a picture”.

Language Translation Demo

The first demo was the language translation. So you could look at a sign, select the translation you want, for example German to English and it would display the sign in English for you.

Music Demo

After that the next demo involved music playing. Google glass would identify the music and display the lyrics to you. That was pretty cool I thought.

Astronomy App Demo

The final demo showed an astronomy app, this used the bone conduction transducer, which means that you can hear Google glass talking to you without headphones. This didn’t work so well because the room was quite noisy.

Finally The Photo

The final part was photos, I chose a less conspicuous pair, being the anonymous kind of guy that I am. So I enjoyed the demo which coincided nicely with delays at Waterloo.

JustinBannisterWithGoogleGlass
Me wearing Google Glass

The Shortcomings

I guess the main shortcoming is that you have to be near WiFi or have a tethering option on your mobile phone contract. Otherwise its just a glorified camera without either of these.

The frames are made of titanium and they do not fold, so you would need to buy a case to put them in when they are not being used.

Conclusion

I was invited to purchase a beta version on the explorers program, but there is a £1000 price tag. For that kind of money I would want a clear idea in my head of what kind of app I would want to develop. But it looks like “wearables” are the future. I would be interested to try the new version of Google Glass, to see how much it has moved on.

Please note, this was my experience with the beta version of Google Glass back in 2014, I’m sure its much more polished now. If you would like to read about the new version of Google Glass, then you can here.

You can purchase your Google Glass here.

Junior Developer Job Hunting Tips

Last year some junior developers which I have worked with in the past asked me for some job hunting tips. I decided to post the advice that I gave them to hopefully help somebody else.

Pick Your Target Job

The first of the job hunting tips is to pick your target job. There are so many different types of development jobs out there, pick a job that you feel passionate about or already have some experience in.

Make a Plan

Now that you have picked the development job that you would eventually like to do, start making a learning plan. This might include the following steps:

  • Make a list of the development stack that you need to learn. You will have to research this, try stack overflow for instance.
  • Make a list of resources to learn about the technologies which make up the development stack. For example books, blogs and online training. A good online training provider is Pluralsight.
  • Join a Meetup group where you can learn from people that are already where you want to be. Also you find people in a similar position and you can learn with and support each other.

Execute the Plan

Study

Schedule in some time with a planning tool like Kanban flow or your diary to make time to learn the material. Make it a regular thing in your day so it becomes a habit.

Use you time Effectively

For instance I used to buy technical books and read them in my lunch breaks and spare time. The spare time could be time travelling on public transport, waiting for public transport.

Practice

While you are learning, it is a good idea to put into practice what you have learnt. An excellent way to do this is build and example application, that way you will challenge what you think that you know. You also have something to show for it at the end. You can use the same technique to learn in on other development stacks in the future.

Stack Overflow

Create a stack overflow account and see if there are any questions which you can answer. If you have questions from building an example application, ask them here.

Curriculum Vitae

Create yourself a curriculum vitae to highlight your skills and experience. Get other people to proof read it and give you feedback.

Prepare For Techical Interviews

Look for interview questions online and test yourself to see what you do and don’t know. Be patient with yourself and fill the gaps in your knowledge.

Find coding tests on the web and attempt to do the tests, again this will show you what you know and what you don’t know. List what you have struggled with and work to fill the gaps in your knowledge.

Find people who are willing to review your code and tell you how you can improve. Ask people that you know who are already experienced. Or if you don’t know anybody, use online forums. You may find it surprising how willing people are to help. People tend to respond well to enthusiasm.

Also search for common interview questions and practice talking about your experience.

Job Hunting

Find job boards and post your CV on the job boards. Schedule in some time each day to search for jobs. Apply for the jobs and keep searching. Eventually you will get an interview, keep practising and preparing.

Technical Screening Test

You will most likely have a technical screening test over the telephone to determine if you should be brought in for a face to face interview. At the end of this take time to note which questions you struggled with and fill the gaps in your knowledge. If you did not succeed, don’t loose heart, keep trying.

Face To Face

In the face to face interview, the interviewers will want to delve deeper into any experience you have and have more technical questions. They will probably give you a timed programming test. This might be sorting algorithms, data structures etc. So make sure you are comfortable implementing these under time pressure.

You will get interviews, some you might fail at, but keep note of this things you fell down on and make sure you fill the gap.

At the end of the day they want to get somebody in with skills that they already have and potentially train you with the ones which you don’t.

Job Offer

Hooray, you have a job offer in a company that you want to work for. All that hard work has payed off, you feel elated and on top of the world. You have achieved your goal.

But what you must realise is that you are embarking on an exciting career. One which is very creative and rewarding. But more importantly you have chosen a career where technology never stands still and you have to be constantly learning and improving. This is what makes a career as a programmer exciting. I wish you the best of luck!

Conclusion

Finding a new job is not always easy, depending on the economic climate that you are in at the time and the skills that have. I hope you found these job hunting tips useful. If you have any other tips that may help other people, please share them below.

Nullable Reference Types

Previous Versions

Previously in C#, references have been allowed to be null and they could be de-referenced without null checks. We would then get a NullReferenceException, this happens when we dereference a variable which is set to null. Sometimes we have nulls coming from other parts of the code, which we then discover later when the variable is de-referenced. We then have to spend time debugging to track down where the null came from in the first place.

New version

In C# 8, the treatment of nulls will become much more stricter than in previous versions of the language. This will take the form of compilation warnings on ordinary reference types like; object, string etc. Nullable Reference Types are not switched on by default, this needs to be switched on at project level or source file level. This can be done with the following line in a source file:

#nullable enable

Or the project file by adding the nullable tag to the property group, see the project file example below:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

The idea behind making this optional, is for large code bases, it will take time to modify the code to fix all the warnings that will be generated when this is switched on. Plus all the additional testing effort to ensure no new bugs have been unwittingly introduced. However, there is talk that in future releases that this will be switched on by default.

Non Nullable Reference Types

A non nullable reference type variable cannot be assigned null and doesn’t need to be checked for null before de-referencing.

In the console application example below, it is possible to get a null reference exception, if the console application is run without specifying an argument.

using System;

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string name = (args.Length > 0) ? args[0] : null;


            PrintName(name);


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }


        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }    
}

If we turn on the Nullable Reference Types feature, like in the example below:

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string name = (args.Length > 0) ? args[0] : null;


            PrintName(name);


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }


        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

We now get some compiler warnings for the console application, see screenshot below:

There is some magic performed by the compiler. So for non nullable reference types, the compiler uses flow analysis to ensure that local variables are assigned a non-null value when they are declared.

We can fix these warnings by removing the null assignment and replacing it with an empty string.

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string name = (args.Length > 0) ? args[0] : "";


            PrintName(name);


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }


        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

Nullable Reference Types

Variables declared as a nullable reference type can be assigned null. They are not subjected to the same checks as their non nullable counterparts. The compiler will use flow analysis to ensure that any nullable reference type variable is checked for null before it is accessed or assigned to a non nullable reference type. The code example below demonstrates the warning. Note that the variable name is defined as a nullable reference type.

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;
            PrintName(name);

            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }

        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

The warning which is generated is in the screenshot below, it relates to the call to PrintName:

Adding the null check before calling PrintName fixes the warning. See the code example below.

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;


            if(name != null)
            {
                PrintName(name);
            }


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }

        private static void PrintName(string name)
        {
            Console.WriteLine(name.Trim());
        }
    }
}

The Null-Forgiving Operator

So what about the times where we know that the value is not null, but the compiler is still warning us? So in the code example below, we have that situation. Please note this is a contrived example.

using System;
#nullable enable


namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;


            Console.WriteLine(name.Trim());


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }
    }
}

Let us assume that the user will always execute the console application with their name as an argument. We can placate the compiler warning by using the null-forgiving operator, the null forgiving operator is ! following the variable name as in name!.Trim(), in the example below:

using System;
#nullable enable

namespace NullableReferenceTypes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string? name = (args.Length > 0) ? args[0] : null;


            Console.WriteLine(name!.Trim());


            Console.WriteLine("Press any key to exit.");
            Console.ReadLine();
        }
    }
}

The compiler warning disappears when using the null forgiving operator.

Conclusion

The Nullable Reference Type feature will prevent a lot of defects in future code bases and improve existing code bases. It is currently switched off by default in C# 8. It will be switched on by default at some point in the future, so existing code bases will need to be updated and re-tested to allow for a smooth transition. Probably something that should be done sooner rather than later, especially if you are producing components which are consumed by others.

One other nice thing about using non nullable reference type variables is that we will not have to do any defensive programming when we de-reference them. This will reduce the amount of code that we have to write in the future.

The null-forgiving operator is a nice touch, to tell the compiler not worry. But there is always that chance that we have got it wrong, so having good unit tests and code reviews in place will make this less likely.

After code bases have been updated, it would probably be a good idea to switch on “treat all warnings as errors” on each project in a solution, to ensure warnings are dealt with and not ignored. If you go down that route, it would be sensible to have a unit test to inspect each project file to ensure that new projects are added with “treat all warnings as errors” and that it is not switched off accidentally.

Read-only Auto Properties

In this article I discuss read-only automatic properties. I am going to cover how automatic properties have changed over the releases of C# and along the way show how much more concise the code becomes.

Automatic Properties

Ever since C# 3.0 came out we had the ability to define automatic properties. Gone were the days of defining properties this way:

public class Dog
{

  private string _name;
  public Dog(string name)
  {
    Name = name;
  }

  public string Name
  {
    get { return _name; }
    set { _name = value; }
  }
}  

Or so we thought, we had this new super automatic properties syntax, that would save our fingers and keep us working as programmers for a bit longer.

When using the automatic property syntax, the compiler creates a private anonymous backing field, this can only be accessed through the property’s get and set accessors.

public class Dog
{
  public Dog(string name)
  {
    Name = name;
  }

  public string Name { get; set; }
}  

Read-Only Property

But how about if we want to make the Name property read-only? Well we would have to revert back to our original property syntax.

public class Dog
{
  private readonly string _name;
  public Dog(string name)
  {
    _name = name;
  }

  public string Name
  {
    get { return _name; }
  }
}  

Read-only Automatic Properties

When C# 6 came out, we were introduced to read-only automatic properties, this improves the above code with a much more concise syntax.

public class Dog
{
  public Dog(string name)
  {
    Name = name;
  }


  public string Name { get; }
}

Conclusion

We can create immutable types with a more concise syntax, i also think this syntax has the benefit of being more readable.

String Interpolation

For years we have all used string.Format, it was one of those constructs that I thought “oh wow”, when I first started using the .Net Framework back in 2004. One of my colleagues at the time remarked “Its sweet, isn’t it?”, I guess we were easily amused back then? In C# 6 we saw the introduction of string interpolation.

So going back to string.format, this line of code is an example of what we would have used:

var name = "fido";
var message = string.Format("My dog's name is {0}", name);
Console.WriteLine(message);

So in the example above, this would output “My dog’s name is fido”.

Now looking at an example with string interpolation we can do this:

var name = "fido";
var message = $"My dog's name is {name}";
Console.WriteLine(message);

This would also output the same of the previous piece of code, but it looks much more compact. In the example above, notice the special $ character. This character identifies the string as an interpolated string. This allows you to embed expression values into a literal string.

You may then wonder, aha, I can do this:

var string name = "fido";
var message = "My dog's name is {name}";
Console.WriteLine($message);

But this code above won’t compile. You cannot parameterise the format string.

Function calls

You can call a function from within an interpolated string, look at the following code below:

Console.WriteLine($"{DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss")}");

But you probably wouldn’t want to do that in this case, you would probably do this instead:

Console.WriteLine($"{DateTime.UtcNow: dd-MM-yyyy HH:mm:ss}");

The above code is the formatting expression syntax, its much more compact than the previous example.

So what happens at the compiler level?

The compiler generates a string.Format in the IL for string interpolation, so this feature is more syntactic sugar. The string literal has to be static, you could not define dynamically. Unlike string.Format, where you can specify a format string at runtime.

Conclusion

I think the string interpolation syntax is much more readable than the old string.Format syntax.

You don’t have to worry about the parameter ordering like you did with string.Format or missing out a parameter in the format string parameter by accident.

You will probably save yourself some typing using this syntax.

If you want to know more please delve into the documentation.

Watch Out For

You have to remember to add the $ special character at the beginning of the string, otherwise you would introduce an unintentional bug. That would not be cool!

nameof Operator

The nameof operator was introduced in C# 6. But before I discuss what the nameof operator is for, I would like to show what life was like before the nameof operator.

Life before the nameof operator

So back in C# 2, we might have the following code in our constructor:

public class Dog
{
  public Dog(string name)
  {
    if(string.IsNullOrEmpty(name))
    {
      throw new ArgumentNullException("name");
    }
  }
}

We would have used magic strings for the null argument in question, but what if we change the argument name down the line and forget to change the string. This isn’t particularly great and is brittle as I mentioned. We could mitigate the risk of this happening by writing a unit test.

You may have built a WPF app with data binding and written code like this:

private string _fullName;
public string FullName
{
    get
    {
        return _fullName;
    }
    set
    {
        _fullName = value;
        NotifyPropertyChanged("FullName");
    }
}

We risk breaking the data binding by changing the name of the property and forgetting to update the string in the call to NotifyPropertyChanged. This might not be noticed, and we have then unwittingly introduced a bug. We could improve the situation by writing a static helper function. We might have written some code similar to this:

public static string NameOfProperty<TSource, TProperty>(Expression<Func<TSource, TProperty>> property)
{

   MemberExpression member = property.Body as MemberExpression;

   if (member == null)
   {
      const string message = "Expression {0} refers to a method.";
      throw new Exception(string.Format(message, property));
   }

   PropertyInfo propertyInfo = member.Member as PropertyInfo;

   if (propertyInfo == null)
   {
      const string message = "Expression {0} refers to a field.";
      throw new Exception(string.Format(message, property));
   }
   
    return propertyInfo.Name;
}

So the property above could then be re-written like so:

private string _fullName;
public string FullName
{
    get
    {
        return _fullName;
    }
    set
    {
        _fullName = value;
        NotifyPropertyChanged(PropertyHelper.NameOfProperty(ViewModel prop) => prop.FullName);
    }
}

Now we have avoided the magic string with this code and the brittleness of it. We have compile time safety as well, so if we change the name of the property, we are forced to fix it when NotifyPropertyChanged is called. This helper method would have come in handy when using Linq to Xml to write or read some xml.

Behold the nameof operator

But now with the nameof operator, life is so much easier. So for the constructor code, we could simply write the following:

public class Dog
{

  public Dog(string name)
  {
    if(string.IsNullOrEmpty(name))
    {
      throw new ArgumentNullException(nameof(name));
    }
  }
}

The nameof operator is evaluated at compile time, so we now have the compile time checking built in, how cool is that!

Similarly the property code can be improved in the same way:

private string _fullName;
public string FullName
{
    get
    {
        return _fullName;
    }
    set
    {
        _fullName = value;
        NotifyPropertyChanged(nameof(FullName));
    }
}

Conclusion

I think the nameof operator was a great addition to C# 6 and has definitely improved the quality of code and the robustness of the code we produce. I think the readability of the code is also improved. It stops those little bugs creeping in to code when we least expect it.

C# Expression Bodied Members

We first saw expression bodied members introduced in C# 6. This introduced the ability to take a single expression method and read-only property and reduce it to a more concise form.

Functions

For example a single line function can be reduced from this:

public void WriteName(string name)
{
   Console.WriteLine(name);
}

To the following code below:

public void WriteName(string name) => Console.WriteLine(name);

Read-only Properties

A read-only property can be reduced from the following:

public string Firstname { get; set; }

public string Surname { get; set; }

public string FullName
{
  get { return Firstname + " " + Surname; }
} 

This read-only property can now be condensed to the following code below:

public string Firstname { get; set; }

public string Surname { get; set; }

public string FullName => Firstname + " " + Surname;

Now moving forward to C# 7, we now have support for: Property, Constructor, Finalizer and Indexer.

Properties

So for a property previously defined like this:

private string _name;

public string Name
{
  get { return _name; }
  set { _name = AlterName(value); }
}

private string AlterName(string name)
{
  return name.ToUpper();
}

This can now be re-written like so:

private string _name;

public string Name 
{
  get => _name;
  set => AlterName(value);
}

private string AlterName(string name)
{
  return name.ToUpper();
}

Constructors

A constructor written before C# 7 would look like this:

private string _name;
public Dog(string name)
{
   _name = name;    
}

With the expression bodied member syntax, the constructor definition becomes:

public class Pets
{
  private string[] _myPets = { "Cat", "Dog", "Rabbit", "Hamster" };

  public string this[int position]
  {
     get
     {
       return _myPets[position];
     }
     set
     {
       _myPets[position] = value;
     }
  }
}

The indexer becomes this in the C# 7 syntax:

public class Pets
{
  private string[] _myPets = { "Cat", "Dog", "Rabbit", "Hamster" };

  public string this[int position]
  {
     get => _myPets[position];
     set => _myPets[position] = value;
  }
}

Finalizers

A finalizer which can be written like this:

private string _name;

~Dog() => Console.WriteLine($"Destructor is executing for {_name}");

So what happens to the intermediate language?

If you inspect the intermediate language for the two different syntaxes, the generated intermediate language looks the same.

Conclusion

The benefits of using Expression Body Members over the older syntax are:

  • More compact code
  • More readable code
  • Syntactic sugar

For me, I prefer the expression bodied member syntax, for the reasons listed above. But you still might prefer the older syntax and find that more readable. So what do you think of the expression bodied member syntax?

.Net Core – Testing Internal Methods

In the .Net Framework, the approach to unit testing internal methods or types was to add an InternalsVisibleTo attribute into the AssemblyInfo.cs file. For example look at the following line of code below:

[assembly: InternalsVisibleTo("Animals.Test")]

This all works fine in the .net Framework. However in .Net Core most of the settings in the AssemblyInfo file have moved to the project file. So to test internal methods, you have 2 options.

Project File Change

Assuming we have a project called Animals and a unit test project called Animals.Tests. The Animals project file which contains the internal method can be modified in visual studio with the following:

  <ItemGroup>
    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
      <_Parameter1>$(MsBuildProjectName).Test</_Parameter1>
    </AssemblyAttribute>
  </ItemGroup>

Source File change

The alternative way is to use an attribute in the source file which contains the internal method. For example see the code below:

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Animals.Test")]

namespace Animals
{
    public class Dog
    {
        internal string Chase(string animal)
        {
            return $"Dog chases {animal}";
        }
    }
}

Conclusion

Out of the two methods above, I much prefer the project file approach, since this is the most flexible. I don’t like the source file approach, since this would mean sprinkling the attribute over the source code.