Hello everyone,

I hope you all had a nice christmas time.

I started to refactor several parts of the Freenet Code to make the it
easier to test and understand the different parts of it in isolation.
Before I continue doing so, I would like to know your thoughts on my
approach and whether you would actually welcome these changes. You can
find the latest version of the partially refactored code on Github [1].
I recently rebased my code so that all commits fit the current state of
the next-branch.

Essentially, I did the following three changes to the Node-,
NodeCrypto-,PeerManager-,UdpSocketHandler-,NodeIPPortDetector- and
IPAddressDetector-class:

1. Create corresponding Interfaces for each class. This allows us to
replace actual class instances (e.g. of the Node class) with a
test-specific dummy implementation. This dummy can then be given as
parameter to a constructor.

2. Encapsulate instantiation of class members within protected methods.
By overriding these protected methods, we can replace class members with
test-specific dummy implementations.

3. Create getter-Methods to access class members. Many classes in
Freenet hold a reference to a Node-instance and some directly access its
members. Besides the fact that this is not a good practice, it kept me
from creating an interface for the node class.

One obstacle with point (1) was that there are several cases where a
class calls a non-public method from another class in the same package.
To illustrate my changes, consider the following three example classes:

package example.A;
class MyClass {

    boolean getBool() { ... }

    public String getString() { ... }   

}

package example.A;
class SamePackageClass {
  public SamePackageClass(MyClass ref) {

    if (ref.getBool()) ...

  }

}

package example.B;
class OtherPackageClass {

  public OtherPackageClass(MyClass ref) {

    process(ref.getString());

  }

}

Because SamePackageClass is within the same package as MyClass, the
getBool-method is visible to the former wheras only getString() is
visible for the OtherPackageClass. After my refactoring, we now have the
following 2 interfaces:

package example.A;
public interface MyClass {

    String getString();

}

package example.A;
interface ProtectedMyClass extends MyClass {

    boolean getBool();

}

Please note that the first interface is a public interface while the
second interface is package-local. The three example classes now look as
follows:

package example.A;
class MyClassImpl implements ProtectedMyClass {

    public boolean getBool() { ... }

    public String getString() { ... }   

}

package example.A;
class SamePackageClass {
  public SamePackageClass(ProtectedMyClass ref) {

    if (ref.getBool()) ...

  }

}

package example.B;
class OtherPackageClass {

  public OtherPackageClass(MyClass ref) {

    process(ref.getString());

  }

}

The getBool-Method of the MyClassImpl-class is now public. However, if
instances of this class are only accessed via the interfaces it is only
visible to classes that use the ProtectedMyClass interface. This way, we
maintain the package-local view for the different methods. What do you
think about this solution?


Given the above classes, an example for a point (2)-refactoring is as
follows:

class AnotherClass {

    MyClass ref;

    public AnotherClass(int param) {

      ref = newMyClass(param);

    }

    protected MyClass newMyClass(int param) {

        return new MyClassImpl(param);

    }

}

Instead of calling "new MyClassImpl(param)" directly in the constructor,
I use a protected method newMyClass. When writing a test case, I can now
easily test AnotherClass without needing an instance of MyClassImpl. To
do so, I create a minimal class MyClassStub that implements the
MyClass-interface and derive the following class:

class InstrumentedAnotherClass extends AnotherClass {

    public InstrumentedAnotherClass(int param) {
      super(param);
    }

    @Override
    protected MyClass newMyClass(int param) {

      return new MyClassStub();

    }

}

As a proof of concept for this approach, I created some initial unit
tests for the NodeCrypto[2]- and NodeIPPortDetector-class[3].

To me, two points that are still open when it comes to tests are:

- Testing of time-based conditions: To test functionalities such as
rate-limiting of requests, the calls to System.currentTimeMillis()
probably need to be replaced by a call to a non-system-interface with a
similar method, whose implementation can then be adapted to test cases.

- Concurrency: I didn't yet spend much time thinking about how classes
can be tested that wait for other threads to operate (e.g. location
swapping) but I am optimistic that this can be done.


Kind regards,
Martin


[1] https://github.com/yadevel/fred/commits/testing-refactoring

[2]
https://github.com/yadevel/fred/blob/testing-refactoring/test/freenet/node/NodeCryptoTest.java

[3]
https://github.com/yadevel/fred/blob/testing-refactoring/test/freenet/node/NodeIPPortDetectorTest.java

-- 
Martin Byrenheid
Scientific Assistant

Technische Universität Dresden
Faculty of Computer Science
Institute of System Architecture
Chair of Privacy and Data Security
01062 Dresden

Tel.: +49 351 463 38235
GPG : F144 FBCD F0C2 257A 87ED 
      CFDE 24EB E684 D0BE E785

Reply via email to