How do you write unit test for [serializefields]

Im doing like this because i dont like to expose fields when i dont need (like giving set acces when i dont need in runtime code) nor use reflection.

    [System.Serializable]
    internal sealed class EntityAttributeData {
        [SerializeField] bool fillAtStart = true;
        [SerializeField] bool fillOnStatChange;
        [SerializeField] bool clampToMax = true;
        [SerializeField] bool clampToZero = true;

        internal bool FillAtStart => fillAtStart;
        internal bool ClampToMax => clampToMax;
        internal bool ClampToZero => clampToZero;
        internal bool FillOnStatChange => fillOnStatChange;

#if UNITY_EDITOR
        internal void Test_SetFillAtStart(bool testValue) => fillAtStart = testValue;
        internal void Test_SetFillOnStatChange(bool testValue) => fillOnStatChange = testValue;
        internal void Test_SetClampToMax(bool testValue) => clampToMax = testValue;
        internal void Test_SetClampToZero(bool testValue) => clampToZero = testValue;
#endif
    }

I wonder if anyone has better way to test them

Can you show an example test case that you want to write?

If the class needs these values, then you have to have a GameObject in your test scene with these values set to what you want. Then exercise the class, eg call some of its methods. The serialized fields are internals of the class and shouldn’t be tested nor set directly.

You could create a number of prefabs with different serialized values. Then instantiate one prefab in each test and check if the object or its components operate correctly. What you needn’t check is whether the serialized values were loaded correctly because Unity guarantees that.

If it’s about checking if the value is valid (eg within range, not null, etc) program that into OnValidate. That’s what OnValidate is for.

The values you show kinda make me thing you don’t even want these in a MonoBehaviour. Instead, you could create a [Serializable] class or struct with these values in it, and possibly even some logic. Then you can normally instantiate this class/struct because it’s not derived from UnityEngine.Object and exercise its logic through tests. That’s the preferred way for anything that doesn’t heavily interact with other components or Unity APIs.

2 Likes

You don’t. They are implementation details not inputs or outputs. The idea is that the internals can change without affecting the outcomes.

If you really want to test something then you can always use reflection to pull the data out. But eventually if you do that enough you’ll find out why it’s a bad idea.

1 Like

im dump, i forget that i can use constructor in serialzable classes if i implement default constructor. Im testing plain classes so yeah default constructor is best option, thanks.

Here is my class

using UnityEngine;

namespace iCare.StatSystem {
    [System.Serializable]
    internal sealed class EntityAttributeData {
        [SerializeField] bool fillAtStart = true;
        [SerializeField] bool fillOnStatChange;
        [SerializeField] bool clampToMax = true;
        [SerializeField] bool clampToZero = true;

        internal bool FillAtStart => fillAtStart;
        internal bool ClampToMax => clampToMax;
        internal bool ClampToZero => clampToZero;
        internal bool FillOnStatChange => fillOnStatChange;

#if UNITY_EDITOR
        internal void Test_SetFillAtStart(bool testValue) => fillAtStart = testValue;
        internal void Test_SetFillOnStatChange(bool testValue) => fillOnStatChange = testValue;
        internal void Test_SetClampToMax(bool testValue) => clampToMax = testValue;
        internal void Test_SetClampToZero(bool testValue) => clampToZero = testValue;
#endif
    }

    public sealed class EntityAttribute {
        float _currentValue;
        readonly bool _clampToMax;
        readonly bool _fillOnStatChange;
        readonly bool _clampToZero;
        readonly EntityStat _maxValueStat;

        public float CurrentValue {
            get => _currentValue;
            set {
                var isReduced = value < _currentValue;

                if (_clampToMax) {
                    _currentValue = Mathf.Clamp(value, 0, _maxValueStat);
                }
                else if (_clampToZero) {
                    _currentValue = Mathf.Max(value, 0);
                }
                else {
                    _currentValue = value;
                }

                if (isReduced) {
                    OnReduceEvent?.Invoke(_currentValue);
                }
                else {
                    OnIncreaseEvent?.Invoke(_currentValue);
                }

                OnValueChangedEvent?.Invoke(_currentValue);
            }
        }

        public event System.Action<float> OnValueChangedEvent;
        public event System.Action<float> OnReduceEvent;
        public event System.Action<float> OnIncreaseEvent;


        internal EntityAttribute(EntityAttributeData data, EntityStat maxValueStat) {
            _maxValueStat = maxValueStat;

            _clampToMax = data.ClampToMax;
            _fillOnStatChange = data.FillOnStatChange;
            _clampToZero = data.ClampToZero;

            if (data.FillAtStart) {
                CurrentValue = _maxValueStat;
            }
        }

        internal void SubscribeStatEvents() {
            if (!_fillOnStatChange) return;
            _maxValueStat.OnValueChangedEvent += OnStatChange;
        }

        internal void UnsubscribeStatEvents() {
            if (!_fillOnStatChange) return;
            _maxValueStat.OnValueChangedEvent -= OnStatChange;
        }

        void OnStatChange(float newStatValue) => ResetToMax();

        public void ResetToMax() => CurrentValue = _maxValueStat;
    }
}

and here is some tests

    public sealed class EntityAttributeTests {
        EntityStat _mockStat;
        EntityAttribute _entityAttribute;

        [SetUp]
        public void SetUp() {
            _mockStat = new EntityStat();
        }

        [Test]
        public void CurrentValue_IsSetToMax_WhenFillAtStartIsTrue() {
            // Arrange
            var data = new EntityAttributeData();
            data.Test_SetFillAtStart(true);
            data.Test_SetClampToMax(true);
            _entityAttribute = new EntityAttribute(data, _mockStat);

            // Act
            _mockStat.Test_SetBaseValue(100f);
            _entityAttribute = new EntityAttribute(data, _mockStat);

            // Assert
            Assert.AreEqual(_mockStat.MaxValue, _entityAttribute.CurrentValue);
        }

        [Test]
        public void CurrentValue_IsClampedToMax_WhenSetAboveMax() {
            // Arrange
            var data = new EntityAttributeData();
            data.Test_SetClampToMax(true);
            _entityAttribute = new EntityAttribute(data, _mockStat);

            var maxValue = 100f;
            _mockStat.Test_SetBaseValue(maxValue);

            // Act
            _entityAttribute.CurrentValue = maxValue + 50;

            // Assert
            Assert.AreEqual(maxValue, _entityAttribute.CurrentValue);
        }

        [Test]
        public void CurrentValue_IsSetToZero_WhenSetBelowZero_IfClampToZeroEnabled() {
            // Arrange
            var data = new EntityAttributeData();
            data.Test_SetClampToZero(true);
            _entityAttribute = new EntityAttribute(data, _mockStat);

            // Act
            _entityAttribute.CurrentValue = -10;

            // Assert
            Assert.AreEqual(0, _entityAttribute.CurrentValue);
        }

        [Test]
        public void OnValueChangedEvent_IsTriggered_WhenCurrentValueChanges() {
            // Arrange
            var data = new EntityAttributeData();
            _entityAttribute = new EntityAttribute(data, _mockStat);

            var eventTriggered = false;
            _entityAttribute.OnValueChangedEvent += value => eventTriggered = true;

            // Act
            _entityAttribute.CurrentValue = 50;

            // Assert
            Assert.IsTrue(eventTriggered);
        }

        [Test]
        public void OnReduceEvent_IsTriggered_WhenValueIsReduced() {
            var stat = new EntityStat();
            stat.Test_SetBaseValue(100);
            
            var data = new EntityAttributeData();
            data.Test_SetFillAtStart(true);
            
            var entityAttribute = new EntityAttribute(data, stat);
            entityAttribute.SubscribeStatEvents();
            
            var eventTriggered = false;
            entityAttribute.OnReduceEvent += value => eventTriggered = true;
            
            entityAttribute.CurrentValue = 50;
            Assert.IsTrue(eventTriggered);

            var event2Triggered = false;
            entityAttribute.OnReduceEvent += value => event2Triggered = true;
            entityAttribute.CurrentValue = 40;
            
            Assert.IsTrue(event2Triggered);
        }


        [Test]
        public void OnIncreaseEvent_IsTriggered_WhenValueIsIncreased() {
            // Arrange
            var data = new EntityAttributeData();
            _entityAttribute = new EntityAttribute(data, _mockStat);

            var eventTriggered = false;
            _entityAttribute.OnIncreaseEvent += value => eventTriggered = true;

            // Act
            _entityAttribute.CurrentValue = 50;
            _entityAttribute.CurrentValue = 60; // Increasing the value

            // Assert
            Assert.IsTrue(eventTriggered);
        }

I would love to hear if you have any other suggestions beside constructor.

I use onvalidate + odin validator for that kind a stuff in general but i like to assign mock values and test scripts because that way i can test and see bugs faster then creating prefabs for each value.